introduction
Before discussing the pattern of DDD layered architecture, let's review the related knowledge of DDD and layered architecture together.
DDD
DDD (Domain Driven Design, Domain Driven Design) as a software development method, it can help us design high-quality software models. When implemented correctly, the design we accomplish with DDD is exactly how software works.
UL (Ubiquitous Language, Universal Language) is a language shared by teams and one of the most powerful features in DDD. Regardless of your role on the team, as long as you are part of the team, you will use UL. Due to the importance of UL, it is necessary to make each concept clear and unambiguous in its own context, so DDD proposes the mode BC (Bounded Context) in strategic design. UL and BC simultaneously form the two pillars of DDD and they complement each other, i.e. UL has its definite contextual meaning while each concept in BC has a unique meaning.
A business domain is divided into several BCs, which are integrated through Context Map. BC is an explicit boundary within which the domain model exists. A domain model is a software model about a specific business domain. Typically, domain models are implemented through object models, which contain both data and behavior and express accurate business meaning.
In a broad sense, a domain is what an organization does and everything contained within it, representing the entire business system. Since the "domain model" includes the word "domain", we might think that a single, cohesive and fully functional model should be created for the entire business system. However, this is not our goal with DDD. On the contrary, the domain model exists within the BC.
In the practice of microservice architecture, people make extensive use of the concepts and techniques in DDD:
- In microservices, UL should be established first, and then the domain model should be discussed.
- A microservice should not exceed one BC at most, otherwise there will be ambiguous domain concepts in the microservice.
- A microservice should not be smaller than an aggregate, otherwise the complexity of distributed transactions will be introduced.
- The division process of microservices is similar to that of BC, and each microservice has a domain model.
- The integration between microservices can be done through Context Map, such as ACL (Anticorruption Layer).
- It is best to use Domain Events to interact between microservices, so that microservices can remain loosely coupled.
- ...
Layered Architecture
An important principle of layered architecture is that each layer can only be coupled to the layer below it. Layered architecture can be simply divided into two types, namely strict layered architecture and loose layered architecture. In a strictly layered architecture , a layer can only be coupled to layers directly below it, while in a loosely layered architecture , a layer is allowed to be coupled to any layer below it.
The benefits of a layered architecture are obvious. First of all, due to the loose coupling relationship between layers, we can focus on the design of this layer without worrying about the design of other layers, nor do we need to worry that our design will affect other layers, which is of great benefit to improving software quality. Secondly, the layered architecture makes the program structure clear, and it is very easy to upgrade and maintain. To change the specific implementation code of a certain layer, as long as the interface of this layer remains stable, other layers do not need to be modified. Even if the interface of this layer changes, it will only affect the adjacent upper layer, the modification workload is small and the error can be controlled, and it will not bring unexpected risks.
To maintain the advantages of the program's layered architecture, it is necessary to adhere to the loosely coupled relationship between the layers. When designing a program, you should first divide the possible layers, as well as the interfaces provided by this layer and the required interfaces. When designing a layer, try to keep the isolation between layers and use only the interfaces provided by the lower layer.
Regarding the advantages of layered architecture, Martin Fowler gave the answer in the book "Patterns of Enterprise Application Architecture":
- Developers can focus on only one layer in the overall structure.
- It is easy to replace old implementations with new implementations.
- Layer-to-layer dependencies can be reduced.
- good for standardization.
- Conducive to the reuse of logic at all layers.
"No gold is bare, no one is perfect", and the layered architecture inevitably has some defects:
- Reduced system performance. This is obvious because of the addition of an intermediate layer, but it can be improved by caching mechanisms.
- May cause cascading modifications. This modification is particularly evident in the top-down orientation, but can be improved by dependency inversion.
In order to highlight the domain model in each BC, a layered architecture pattern is proposed in DDD. In recent years, the author often uses the layered architecture pattern in the process of practicing DDD. This article mainly shares the three classic patterns in the DDD layered architecture.
Mode 1: Four-tier architecture
Eric Evans proposed the traditional four-tier architecture pattern in the book "Domain-Driven Design - How to Cope with Software Core Complexity", as shown in the following figure:
ddd-l4.png
- User Interface is the user interface layer (or presentation layer), responsible for displaying information to users and interpreting user commands. The user referred to here can be another computer system, not necessarily the person using the user interface.
- Application is the application layer, which defines the tasks to be completed by the software, and directs objects that express domain concepts to solve problems. This layer is responsible for the work that is of great significance to the business, and is also a necessary channel for interaction with the application layer of other systems. The application layer should be as simple as possible, not containing business rules or knowledge, but only coordinating tasks for the domain objects in the next layer, assigning work, and making them cooperate with each other. It has no state that reflects the business situation, but can have another state that shows the user or program the progress of a task.
- Domain is the domain layer (or model layer), responsible for expressing business concepts, business state information and business rules. Although the technical details of saving business state are implemented by the infrastructure layer, the state that reflects the business situation is controlled and used by this layer. The domain layer is the core of the business software, and the domain model is located in this layer.
- The Infrastructure layer is the basic implementation layer, providing common technical capabilities to other layers: delivering messages to the application layer, providing a persistence mechanism for the domain layer, drawing screen components for the user interface layer, and so on. The infrastructure layer can also support the interaction mode between the four layers through the architectural framework.
The traditional four-layer architecture is a limited loose layered architecture , that is, any upper layer of the Infrastructure layer can access this layer (“L” type), while other layers follow a strict layered architecture .
In the practice of the four-layer architecture model, the author defines the localization of layers as follows:
- The User Interface layer is mainly Restful message processing, configuration file parsing, and so on.
- The Application layer is mainly multi-process management and scheduling, multi-thread management and scheduling, multi-coroutine scheduling and state machine management, and so on.
- The Domain layer is mainly the realization of the domain model, including the establishment of domain objects, the life cycle management and relationship of these objects, the definition of domain services, the release of domain events, and so on.
- The Infrastructure layer is mainly a business platform, programming framework, encapsulation of third-party libraries, basic algorithms, and so on.
Note: Strictly speaking, User Interface refers to the user interface. The processing of Restful messages and configuration file parsing should be placed in the Application layer. If there is no User Interface layer, it will be vacant. But User Interface can also be understood as a user interface, so it is OK to put Restful messages and configuration file parsing in the User Interface layer.
Mode 2: Five-layer Architecture
James O. Coplien and Trygve Reenskaug published a paper in 2009, "DCI Architecture: A New Vision for Object-Oriented Programming", marking the birth of the DCI architectural pattern. Interestingly, James O. Coplien is also the creator of the MVC architectural pattern. This uncle has done two things in his life, that is, he created MVC when he was young and created DCI when he was old.
Object-oriented programming is all about unifying the programmer's and user's perspectives in computer code: a boon for improving usability and making programs easier to understand. But while objects reflect structure well, they fail at reflecting the actions of the system. DCI was conceived to reflect the roles and interactions between roles in the end-user's cognitive model.
Traditionally, object-oriented programming languages have not come up with a way to capture the cooperation between objects, and do not reflect the algorithms exchanged in the cooperation. Just as object instances reflect domain structure, object collaboration and interaction are structured. Collaboration and interaction are also part of the end-user mental model, but you won't find a cohesive representation in code to represent them. In essence, roles embody generalized, abstract algorithms. The role has no flesh and blood, and can't do practical things. In the final analysis, the work still falls on the head of the object, and the object itself is also responsible for embodying the domain model.
In people's minds, there are two different models for the unified whole of "object", namely "what the system is" and "what the system does". This is the fundamental problem that DCI wants to solve. Users recognize objects and the fields they represent, and each object must also implement some behaviors according to the interaction model in the user's mind, and connect with other objects through its role in the use case. Because the end user can combine the two perspectives, an object of a class can, in addition to supporting the member functions of the owning class, execute the member functions that act as if those functions belonged to the object itself. In other words, we want to inject the logic of the role into the object, and make that logic part of the object in a position no less than the methods it gets from the class when the object is initialized. All the logic we might need to play a role is arranged for the object at compile time. If we are smarter, we only know the assigned role at runtime, and then inject the logic that is just used, it can also be done.
Algorithms and role-object mappings are owned by Context. The Context "knows" which object should be found to act as the actual actor in the current use case, and is then responsible for "casting" the object into the corresponding character in the scene (the word "cast" means casting in the theater world, and the word used here is at least Conforms to that meaning, and on the other hand is intended to be reminiscent of the meaning of cast in some programming language type systems). In a typical implementation, each use case has a corresponding Context object, and each role involved in the use case also has an identifier in the corresponding Context. All the Context does is bind the role identifier to the correct object. Then we just trigger the "opening" role in the Context, and the code will run.
So we have a complete DCI architecture (Data, Context and Interactive three-tier architecture):
- The Data layer describes the domain concepts of the system and the relationships between them. This layer focuses on the establishment of domain objects and the life cycle management and relationships of these objects, allowing programmers to think about the system from the perspective of objects, so as to make "what is the system?" "Easier to understand.
- Context layer: is the thinnest possible layer. Context is often implemented stateless, just find a suitable role and let the roles interact to complete the business logic. However, simplicity does not mean that it is not important. The explicit context layer provides the entry point and main line for people to understand the software business process.
- The Interactive layer is mainly reflected in the modeling of the role. The role is the real executor of the complex business logic in each context, reflecting "what the system does". What role does is to model behavior, it connects context and domain objects. Because the behavior of the system is complex and changeable, the role makes the system separate the stable domain model layer and the changeable system behavior layer, and the role focuses on modeling the system behavior. This layer is often concerned with the extensibility of the system, which is closer to the practice of software engineering. In object-oriented, it is more about thinking and designing from the perspective of classes.
DCI is currently widely seen as a development and supplement to DDD, used in object-oriented domain modeling. Explicitly modeling the role solves the controversy between the hyperemia model and the anemia model in object-oriented modeling. DCI models behaviors by explicitly using roles, and at the same time allows roles to be bound (cast) to corresponding domain objects in the context, thus not only solving the problem of inconsistent data boundaries and behavior boundaries, but also solving domain objects. The problem of high cohesion and low coupling between data and behavior.
A thorny problem with object-oriented modeling is that data boundaries and behavioral boundaries are often inconsistent. Following the idea of modularity, we encapsulate behavior and its tightly coupled data through classes. However, in complex business scenarios, behaviors often span multiple domain objects. If such behaviors are placed in a certain object, other objects will need to expose their internal state to the object. Therefore, after the development of object-oriented, there are two factions of domain modeling. One tends to model the behavior of objects across multiple domains in domain services. If this practice is overused, it will cause the domain object to become a dummy object that only provides a bunch of get methods. This modeling result is called an anemic model. The other school firmly believes that methods should belong to domain objects, so all business behaviors are still placed in domain objects, which causes domain objects to become god classes as more business scenarios are supported, and the abstraction of methods inside classes Hierarchies are hard to match. In addition, because the behavior boundary is difficult to be appropriate, the data access relationship between objects is also complicated. This modeling result is called the hyperemia model.
Regarding multi-role objects, here is an example from life:
People have multiple roles, and different roles perform different responsibilities:
- As parents: we tell our children stories, play games with them, and put them to sleep.
- As children: We honor our parents and listen to their life advice.
- As a subordinate: we must obey the work arrangement of the boss and complete the task with high quality.
- As a boss: we should arrange the work of subordinates, and cultivate and motivate them.
- ...
Here people (large objects) aggregate multiple roles (small classes), and people can only play a specific role in a certain scenario:
- In front of our children, we are parents.
- Before our parents, we are children.
- In front of the boss, we are subordinates.
- In front of our subordinates, we are the boss.
- ...
After the introduction of DCI, the Domain layer in the DDD four-layer architecture model has become thinner. Previously, the Domain layer corresponds to the three layers in DCI, but now:
- The Domain layer only retains the Data layer and Interaction layer in DCI. In practice, we usually use directories to isolate these two layers, that is, to separate the layers Data and Interaction through two directories, object and role.
- The Context layer in DCI is moved up from the Domain layer to the Context layer.
Therefore, the DDD layered architecture pattern becomes five layers, as shown in the following figure:
ddd-l5.png
In practice, the author defines the localization of these five layers as:
- User Interface is the user interface layer, which is mainly used to process Restful requests sent by users, parse user input configuration files, etc., and pass information to the interface of the Application layer.
- The Application layer is the application layer, responsible for multi-process management and scheduling, multi-thread management and scheduling, multi-coroutine scheduling, and maintaining the state model of business instances. When the scheduling layer receives the request from the user interface layer, it entrusts the Context layer to process the context related to this business.
- Context is the environment layer, and takes the context as a unit to cast the domain objects of the Domain layer into appropriate roles, and let the roles interact to complete the business logic.
- The Domain layer is the domain layer that defines the domain model, including not only the modeling of domain objects and their relationships, but also the explicit modeling of the roles of objects.
- The Infrastructure layer is the basic implementation layer, providing general technical capabilities for other layers: business platform, programming framework, persistence mechanism, message mechanism, encapsulation of third-party libraries, general algorithms, and so on.
Is the discussion of the DDD five-layer architecture pattern finished? The story is not over yet...
Many of the DDD implementation practices that the author has participated in are systems that are oriented to the control plane or management plane and have many message interactions. A business of this type of system includes a sequence of synchronous messages or asynchronous messages. If they are placed in the Context layer, the code of this layer will be complicated, so we consider:
- The Context layer is split into two layers, namely the Context layer and the large Context layer, in systems that face the control plane or the management plane and have many message interactions.
- The processing unit of the Context layer is Action, which corresponds to a synchronous message or an asynchronous message.
- The large Context layer corresponds to a transaction, which consists of an Action sequence, which is generally implemented through the Transaction DSL, so we are used to calling the large Context layer the Transaction DSL layer.
- The Application layer often does some scheduling-related work in systems that face the control plane or management plane and have many message interactions, so we are used to calling the Application layer the Scheduler layer.
Therefore, in systems that face the control plane or management plane and have many message interactions, the DDD layered architecture pattern becomes six layers, as shown in the following figure:
ddd-l6.png
In practice, the author defines the localization of these six layers as:
- User Interface is the user interface layer, which is mainly used to process Restful requests sent by users, parse configuration files input by users, etc., and pass information to the interface of the Scheduler layer.
- Scheduler is the scheduling layer, responsible for multi-process management and scheduling, multi-thread management and scheduling, multi-coroutine scheduling and maintaining the state model of business instances. When the scheduling layer receives the request from the user interface layer, it entrusts the Transaction layer to process the transactions related to this operation.
- Transaction is the transaction layer, corresponding to a business process, such as UE Attach, which combines the processing sequences of multiple synchronous messages or asynchronous messages into a transaction, and in most scenarios, there is a selection structure. In the event of a transaction failure, it will be rolled back immediately. When the transaction layer receives the request from the scheduling layer, it entrusts the Action of the Context layer to process it, often accompanied by the use of the Specification (predicate) of the Context layer to select the Action.
- Context is the environment layer, which uses Action as the unit to process a synchronous message or asynchronous message, cast the domain object of the Domain layer into a suitable role, and let the roles interact to complete the business logic. The environment layer usually also includes the implementation of Specification, that is, a conditional judgment is completed through the knowledge of the Domain layer.
- The Domain layer is the domain layer that defines the domain model, including not only the modeling of domain objects and their relationships, but also the explicit modeling of the roles of objects.
- The Infrastructure layer is the basic implementation layer, providing general technical capabilities for other layers: business platform, programming framework, persistence mechanism, message mechanism, encapsulation of third-party libraries, general algorithms, and so on.
The core of the transaction layer is the transaction model, and the framework code of the transaction model is generally placed in the infrastructure layer. Regarding the transaction model, the author shared an article before - "Golang Transaction Model", and interested students can take a look.
To sum up, the DDD six-tier architecture can be regarded as a variant of the DDD five-tier architecture in a specific field. We collectively call it the DDD five-tier architecture, and the DDD five-tier architecture is similar to the traditional four-tier architecture, both of which are loosely limited. Layered Architecture .
Mode 3: Hexagonal Architecture
One way to improve layered architecture is the Dependency Inversion Principle (DIP), which improves by changing the dependencies between different layers.
The Dependency Inversion Principle was proposed by Robert C. Martin and is formally defined as:
High-level modules should not depend on low-level modules, both should depend on abstractions.
Abstractions should not depend on details, and details should depend on abstractions.
According to this definition, the low-level components in the DDD layered architecture should depend on the interfaces provided by the high-level components, that is, both the high-level and the low-level depend on abstraction, and the entire layered architecture seems to be flattened. If we flatten the layered architecture and add some symmetry to it, there is an architectural style characterized by symmetry, the hexagonal architecture. The hexagonal architecture was proposed by Alistair Cockburn in 2005, in which different clients interact with the system in an "equal" manner. Need new clients? not a problem. Just add a new adapter to convert client input into parameters that can be understood by the system API. At the same time, for each specific output, there is a new adapter responsible for completing the corresponding conversion function.
The hexagonal architecture is also known as ports and adapters, as shown in the following diagram:
ddd-hex.png
Each different side of the hexagon represents a different type of port, which either handles input or output. For each external type, there is an adapter corresponding to it, and the external interacts with the internal through the application layer API. In the figure above, there are 3 client requests that all arrive at the same input ports (adapters A, B, and C), and another client request uses adapter D. Suppose the first 3 requests use the HTTP protocol (browser, REST, SOAP, etc.), and the second request uses the AMQP protocol (such as RabbitMQ). Ports are not clearly defined, it is a very flexible concept. No matter which method is used to divide the port, when the client request arrives, there should be a corresponding adapter to convert the input, and then the port will call an operation of the application or send an event to the application, and the control will be transferred from this. to the inner area.
The application receives client requests through a public API and uses the domain model to process the requests. We can think of the implementation of Repository, a modeling element of DDD tactical design, as a persistence adapter that is used to access previously stored aggregate instances or save new aggregate instances. As shown by adapters E, F and G in the figure, we can implement the repository in different ways, such as relational database, document-based storage, distributed cache or in-memory storage, etc. If the application sends domain event messages to the outside world, we will use adapter H to handle it. This adapter handles message output, while the adapter mentioned above that handles AMQP messages handles message input, so a different port should be used.
In our actual project development, components of different layers can be developed at the same time. When the function of a component is clear, development can begin immediately. Since there are multiple users of this component, and these users have different focuses, it is necessary to provide multiple different interfaces. At the same time, the understanding of these users is also deepening, and related interfaces may be refactored many times. Therefore, multiple users of the component often ask the developer of the component to discuss these issues, which invisibly reduces the development efficiency of the component.
Let's put it another way, component developers focus on function development after clarifying the function of the component to ensure the function is stable and efficient. The user of the component defines the interface (port) of the component, and then writes tests based on the interface, and continuously evolves the interface. In cross-layer integration testing, it is sufficient to develop another adapter by the component developer or user.
The Evolution of the Hexagonal Architecture Pattern
While the hexagonal architectural pattern is good, there is no end to the best, and there is no end to the evolution. In the years after the hexagonal architecture model was proposed, three variants of the hexagonal architecture model have been derived in turn. Interested readers can click the link to learn by themselves:
- Jeffrey Palermo proposed the onion architecture in 2008, and the hexagonal architecture is a superset of the onion architecture.
- Robert C. Martin proposed Clean Architecture in 2012, which is a variant of the hexagonal architecture.
- Russ Miles proposed the Life Preserver design in 2013, a design based on a hexagonal architecture.
summary
This article first reviews the relevant knowledge of DDD and layered architecture with readers, and then elaborates on the three commonly used modes (four-layer architecture, five-layer architecture and hexagonal architecture) in DDD layered architecture combined with practical experience. It enables readers to deeply understand the DDD layered architecture mode, so as to select the most appropriate DDD layered architecture mode according to the specific situation in the development practice of microservices, so as to deliver software products with a clear structure and easy maintenance.