07 Ddd Layered Architecture Effectively Reducing Dependencies Between Layers

07 DDD Layered Architecture - Effectively Reducing Dependencies Between Layers #

Hello, I am Ou Chuangxin. Earlier, we discussed some important concepts of DDD and the design principles of domain models. Today, let’s talk about “DDD Layered Architecture”.

There are many types of microservice architectural models, such as Clean Architecture, CQRS, and Hexagonal Architecture, etc. Although each architecture pattern was proposed in different times and contexts, their core idea is to design an architecture that is “highly cohesive and loosely coupled” in order to facilitate architectural evolution. The emergence of DDD Layered Architecture has made architectural boundaries clearer and it holds a very important position in the microservice architectural model.

So what does DDD Layered Architecture look like? How does it facilitate architectural evolution? How can we transition to DDD Layered Architecture? These are the questions we will focus on in this lecture.

What is DDD Layered Architecture? #

The layered architecture of DDD is constantly evolving. Initially, there was the traditional four-layer architecture. Later, the four-layer architecture was optimized further to decouple each layer from the underlying layer. Then, a context layer was added between the domain layer and the application layer, forming the five-layer architecture (DCI).

img

Let’s take a look at the image above. In the earliest traditional four-layer architecture, the underlying layer was dependent on by the other layers, and it was positioned at the core. According to the idea of layered architecture, it should be the core. However, in reality, the domain layer is the core of the software. So, this dependency is problematic. Later, we adopted the Dependency Inversion Principle (DIP) to optimize the traditional four-layer architecture and decouple each layer from the underlying layer.

The DDD layered architecture we are discussing today is the optimized four-layer architecture. In the image below, from top to bottom, there are: the user interface layer, the application layer, the domain layer, and the underlying layer. So, what are the main responsibilities of each layer in DDD? Let me introduce them one by one.

img

  1. User Interface Layer

The user interface layer is responsible for presenting information to users and interpreting user commands. Users can be actual users, programs, automated tests, batch scripts, etc.

  1. Application Layer

The application layer is a thin layer that, in theory, should not contain any business rules or logic. It mainly focuses on operations related to use cases and processes. However, the application layer is positioned above the domain layer because the domain layer contains multiple aggregates. Therefore, the application layer can coordinate the services and domain objects of multiple aggregates to achieve service orchestration and composition, collaborating to complete business operations.

Additionally, the application layer serves as a channel for interaction between microservices. It can call the application services of other microservices to achieve service composition and orchestration between microservices.

I want to remind you that when designing and developing, avoid implementing business logic that should be in the domain layer in the application layer. A bloated application layer will cause the domain model to lose focus, and over time, your microservice will evolve into a traditional three-layer architecture with confusing business logic.

Furthermore, application services are located in the application layer. They are responsible for service composition, orchestration, and forwarding, handling the execution order of business use cases, assembling results, and publishing coarse-grained services to the front-end through an API gateway. Additionally, application services can perform security authentication, permission verification, transaction control, and sending or subscribing to domain events.

  1. Domain Layer The purpose of the domain layer is to implement the core business logic of the enterprise and ensure the correctness of the business through various validation methods. The domain layer mainly embodies the business capabilities of the domain model, which is used to express business concepts, business states, and business rules.

The domain layer includes domain objects such as aggregate roots, entities, value objects, and domain services.

Here, I want to explain the relationships between some of these domain objects so that you can have a clearer understanding when designing the domain layer. Firstly, the business logic of the domain model is mainly implemented by entities and domain services, with entities adopting the rich model that implements all related business functionalities. Secondly, you should know that entities and domain objects are not on the same level in terms of implementing business logic. When certain functionalities in the domain cannot be implemented by a single entity (or value object), domain services will be utilized. They can combine multiple entities (or value objects) within an aggregate to implement complex business logic.

  1. Infrastructure Layer

The infrastructure layer runs through all the other layers and its purpose is to provide common technology and basic services for other layers, including third-party tools, drivers, message middleware, gateways, files, caches, and databases, etc. One of the more common functionalities is providing database persistence.

The infrastructure layer includes basic services, which adopt a dependency inversion design to encapsulate basic resource services and achieve decoupling between the application layer, domain layer, and infrastructure layer, reducing the impact of external resource changes on the application.

For example, in traditional architecture design, due to the tight coupling between the upper-layer application and the database, many companies are most concerned about changing databases during architecture evolution. Because once the database is changed, it may require rewriting a large portion of the code, which is fatal for the application. However, using dependency inversion design, the application layer can maintain independent core business logic through decoupling. When the database changes, we only need to replace the database basic service, minimizing the impact of resource changes on the application.

What is the most important principle of DDD layered architecture? #

In the book “Implementing Domain-Driven Design,” there is an important principle of DDD layered architecture: each layer can only be coupled with the layer directly below it.

According to the degree of coupling, architecture can be divided into two types: strict layered architecture and loose layered architecture. The optimized DDD layered architecture model belongs to strict layered architecture, where any layer can only have dependencies on the layer directly below it. Traditional DDD layered architecture belongs to loose layered architecture, allowing a layer to have dependencies on any layer below it.

So how do we choose? Based on my experience, for the sake of manageability, I suggest using strict layered architecture.

In strict layered architecture, domain services can only be called by application services, and application services can only be called by the user interface layer. Services are exposed or composed layer by layer, and the dependency relationship is clear. In loose layered architecture, domain services can be called by both the application layer and the user interface layer, resulting in complex and difficult-to-manage service dependencies, and even potential exposure of core business logic.

Just imagine, if a major change occurs in a service within the domain layer, how do you notify all the callers to synchronize their adjustments and upgrades? But in strict layered architecture, you only need to notify the upper-layer services layer by layer.

How does DDD layered architecture drive architectural evolution? #

The domain model is not fixed, as changes in the business will affect the domain model, which in turn will affect the functionality and boundaries of microservices. So how do we achieve synchronized evolution of the domain model and microservices?

  1. Evolution of Microservices Architecture

As explained in the fundamentals, we know that the hierarchy of objects in the domain model, from innermost to outermost, is: value objects, entities, aggregates, and bounded contexts.

Simple changes to entities or value objects usually do not result in major changes to the domain model or microservices. However, reorganizing or splitting aggregates can. This is because aggregates have cohesive business functionality and can independently execute specific business logic. Therefore, reorganizing or splitting aggregates will inevitably lead to changes in business modules and system functionality.

We can use aggregates as the basic unit to evolve the domain model and microservices architecture. Aggregates can be reorganized or split between different domain models, or directly separated into independent microservices.

img

Let’s take microservice 1 as an example to explain the process of evolving the microservices architecture:

When you find that the functionality of aggregate A in microservice 1 is frequently accessed and affecting the performance of the entire microservice 1, you can extract the code of aggregate A from microservice 1 and create a separate microservice 2. This way, microservice 2 can easily handle high-performance scenarios.

As the business develops, you may find changes in the domain model of microservice 2, and aggregate D may be better suited to be included in the domain model of microservice 1. Then you can move the code of aggregate D as a whole into microservice 1. If you have already defined the code boundaries between aggregates during the design phase, this process should not be too complicated or time-consuming.

Finally, after going through the evolution of the model and architecture, microservice 1 has evolved from initially containing aggregates A, B, and C to containing the new domain model and microservice with aggregates B, C, and D.

You see, with well-designed boundaries for aggregates and code models, you can quickly adapt to changes in the business and easily achieve the evolution of the domain model and microservices architecture. You might wonder, how can we quickly reorganize the code of aggregates? Don’t worry, the practical part will explain it in detail later. Here, let’s get an overview of the overall implementation process.

  1. Evolution of Services within Microservices

Within a microservice, the methods of entities are combined and encapsulated by domain services, and domain services are combined and encapsulated by application services. During the process of combining and encapsulating services layer by layer, you will notice an interesting phenomenon.

img

Let’s take a look at the above figure. When designing services, you may not be able to fully predict which lower-level services will be assembled by how many upper-level services. Therefore, the domain layer usually only provides some atomic services, such as domain services A, B, and C. However, as the system functionality grows and more external access is required, application services become richer. One day, you will find that domain services B and C are being called multiple times by multiple application services, and the execution order is basically the same. At this point, you can consider merging B and C and sinking the functionality of application services B and C to the domain layer, evolving them into a new domain service (B+C). This reduces the number of services and simplifies the combination and orchestration of upper-level services.

You see, this is the process of service evolution, which occurs as your system develops. In the end, you will find that your domain model becomes more refined and more adaptable to rapid changes in requirements.

How does the three-tier architecture evolve into the DDD layered architecture? #

Based on the previous explanations, I believe you now have a good understanding of the advantages of the DDD layered architecture. Let’s summarize the two most important points.

Firstly, due to the loose coupling between layers, we can focus on the design of the current layer without worrying about other layers or the impact of our design on other layers. It can be said that DDD successfully reduces the dependencies between layers.

Secondly, the layered architecture makes the program structure clear and easier to upgrade and maintain. When we modify the code of a certain layer, as long as the interface parameters of that layer remain unchanged, other layers do not need to be modified. Even if the interface of the current layer changes, it only affects the adjacent upper-layer, and the workload for modification is small and errors can be controlled, without bringing unexpected risks.

So how do we transition to the DDD layered architecture? Let’s take a look at the process below.

Traditional enterprise applications are mostly monolithic architectures, and monolithic architectures are mostly based on the three-tier architecture. The three-tier architecture solves the problems of complex code calling and unclear code responsibilities within the program, but this kind of layering is a logical concept. Physically, it is a centralized and centralized architecture, which is not suitable for distributed microservices architectures.

The elements in the DDD layered architecture are actually similar to the three-tier architecture, but in the DDD layered architecture, these elements are reclassified and the layers are redefined, determining the interaction rules between layers and the boundaries of responsibilities.

img

Let’s take a look at the picture above and analyze the process of transitioning from the three-tier architecture to the DDD layered architecture.

Firstly, you should be clear that the transition from the three-tier architecture to the DDD layered architecture mainly occurs in the business logic layer and the data access layer.

The DDD layered architecture introduces DTO in the user interface layer, providing the frontend with more usable data and higher display flexibility.

The DDD layered architecture provides a clearer division of the business logic layer in the three-tier architecture, improving the situations where the core business logic of the three-tier architecture is chaotic and code changes have a significant impact on each other. The DDD layered architecture separates the services of the business logic layer into the application layer and the domain layer. The application layer responds quickly to frontend changes, and the domain layer implements the capabilities of the domain model.

Another important change occurs between the data access layer and the foundation layer. The three-tier architecture uses the DAO pattern for data access, while the DDD layered architecture uses the Repository design pattern for accessing foundational resources such as databases. This is achieved through dependency inversion to decouple the layers from the foundational resources.

The Repository is divided into two parts: the Repository interface and the Repository implementation. The Repository interface belongs to the domain layer, and the Repository implementation belongs to the foundation layer. The previously common third-party tools, drivers, Common, Utility, Config, and other common public resource classes in the three-tier architecture are unified in the foundation layer.

Finally, I want to say that the evolution from the traditional three-tier architecture to the DDD layered architecture reflects the evolution of the Domain-Driven Design (DDD) concept. I hope you have also felt it and tried to apply it in your own architectural designs.

Summary #

Today we mainly discussed the layered architecture of DDD, which serves as the core framework of microservices. I think it is not excessive to emphasize its importance.

The DDD layered architecture includes the user interface layer, application layer, domain layer, and infrastructure layer. By dividing these layers, we can clarify the functions of each layer in microservices, define the boundaries of each domain object, and determine the collaboration methods of each domain object. This architecture not only reflects the requirements of microservice design and architecture evolution but also integrates well with the concept of domain modeling. The seamless integration of the two will undoubtedly bring a different feeling to your microservice design.