The introduction

Before we discuss DDD layered architecture patterns, let’s review DDD and layered architecture.

DDD

As a software development method, DDD (Domain Driven Design) 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), a team-shared Language, is one of the most powerful features of 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 was necessary to make each concept clear and unambiguous in its own Context, so DDD proposed a pattern BC (limited to the following) in strategic design. UL and BC form the two pillars of DDD, and they complement each other, that is, UL has a defined context meaning, while each concept in BC has a unique meaning. A business domain is divided into SEVERAL BCS, which are integrated through Context Maps. BC is an explicit boundary within which the domain model exists. A domain model is a software model about a particular business domain. Typically, domain models are implemented through object models that contain both data and behavior and express precise business implications. Broadly speaking, a domain is what an organization does and everything in it, representing the entire business system. Since “domain model” includes the word “domain,” we might think that we should create a single, cohesive, and fully functional model for the entire business system. However, this is not our goal with DDD. On the contrary, the domain model exists in BC.

Concepts and techniques from DDD are used extensively in microservices architecture practices:

  1. The UL should be established in microservices first, and then the domain model should be discussed.
  2. A microservice should not exceed a maximum of one BC, otherwise there will be ambiguous domain concepts within the microservice.
  3. A microservice must be at least one aggregation, which introduces the complexity of distributed transactions.
  4. The partitioning process for microservices is similar to the partitioning process for BC, with each microservice having a domain model.
  5. Integration between microservices can be done using Context Maps, such as ACL (Anticorruption Layer).
  6. It is best to use Domain events to interact with microservices so that microservices can remain loosely coupled.
  7. .

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 divided into two types: strictly layered architecture and loosely layered architecture. In a strictly layered architecture, a layer can only be coupled to the layer directly below it, whereas in a loosely layered architecture, a layer is allowed to be coupled to any layer directly below it.

The benefits of a layered architecture are obvious. First of all, because of the loose coupling between layers, we can focus on the design of this layer, and do not have to worry about the design of other layers, and do not have to worry about their design will affect other layers, which is of great benefit to improve the quality of software. Secondly, the hierarchical architecture makes the program structure clear, upgrade and maintenance become very easy, change the specific implementation code of one layer, as long as the interface of this layer remains stable, other layers can not be modified. Even if the interfaces of this layer are changed, only the neighboring upper layers are affected. The modification work is small and the errors can be controlled without any unexpected risks. To maintain the benefits of a layered program architecture, loose coupling between layers must be maintained. When designing a program, you should first identify the possible layers and the interfaces that this layer provides and needs. When designing a layer, try to keep it as separate as possible and use only the interfaces provided by the lower layer. Martin Fowler, in his book Patterns of Enterprise Application Architecture, provides an answer to the question of the advantages of layered architectures:

  1. Developers can focus on just one layer of the entire structure.
  2. It is easy to replace the existing level of implementation with a new one.
  3. Layer to layer dependencies can be reduced.
  4. Good for standardization.
  5. It facilitates the reuse of logic of each layer.

As a Chinese saying goes, “Gold is not perfect and no one is perfect”, the layered architecture inevitably has some defects:

  1. The system performance is reduced. This is obvious because of the added middle tier, but it can be improved by caching mechanisms.
  2. This may result in cascading changes. This modification is particularly evident in the top-down direction, but can be improved by relying on inversion.

In order to highlight the domain model in each BC, the layered architecture pattern is presented in DDD. In recent years, the author has often used the layered architecture pattern in the process of practicing DDD. This article mainly shares three classic patterns in DDD layered architecture.

Pattern 1: Four-tier architecture

Eric Evans, in his book Domain-Driven Design: Coping with Core Complexity in Software, proposes the traditional four-tier architecture pattern, as shown in the figure below:

ddd-l4.png

  1. The User Interface is the User Interface layer (or presentation layer), which is responsible for displaying information to users and interpreting User commands. The user can be another computer system, not necessarily the person using the user interface.
  2. Application is an Application layer that defines the tasks the software is to accomplish and directs objects that express domain concepts to solve problems. This layer is responsible for work that is significant to the business and a necessary channel for interacting with the application layers of other systems. The application layer should be as simple as possible, containing no business rules or knowledge, but only coordinating tasks and assigning work to domain objects in the next layer to make them work together. It does not reflect the state of the business situation, but it can have another state that shows the progress of a task to the user or program.
  3. Domains are the Domain layer (or model layer), which is responsible for expressing business concepts, business state information, and business rules. Although the technical details of preserving the business state are implemented by the infrastructure layer, the state reflecting the business state 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.
  4. The Infrastructure layer is the underlying implementation layer that provides common technical capabilities to other layers: passing messages to the application layer, providing persistence mechanisms to the domain layer, drawing screen components to the user interface layer, and so on. The infrastructure layer can also support interaction patterns between the four layers through an architectural framework.

The traditional four-tier architecture is a restricted loosely layered architecture, that is, any upper layer of the Infrastructure layer can access the layer (” L “type), while the other layers adhere to a strictly layered architecture

In the practice of the four-tier architecture mode, the author mainly defines the localization of layers as follows:

  1. The User Interface layer is mainly Restful message processing, configuration file parsing, and so on.
  2. Application layer is mainly multi-process management and scheduling, multi-thread management and scheduling, multi-coroutine scheduling and state machine management, etc.
  3. Domain layer is mainly the implementation of Domain model, including the establishment of Domain objects, the life cycle management and relationship of these objects, the definition of Domain services, the publication of Domain events, etc.
  4. The Infrastructure layer is a business platform, programming framework, encapsulation of third-party libraries, underlying algorithms, and so on.

Note: Strictly speaking, User Interface refers to the User Interface, Restful messages and configuration file parsing and other processing should be placed in the Application layer, the User Interface layer is empty if there is no. However, a User Interface can also be understood as a User Interface, so processing such as Restful messages and configuration file parsing can be placed in the User Interface layer.

Pattern 2: Five-tier architecture

James O. Coplien and Trygve Reenskaug published a paper “DCI Architecture: New Ideas for Object-oriented Programming” in 2009, marking the birth of the DCI architecture pattern. It’s interesting that James O. Coplien was also the creator of the MVC architecture pattern. He did two things in his life, MVC when he was young and DCI when he was old, and spent the rest of his time thinking, leaving me in the dust. Object-oriented programming is meant to unify the programmer’s and user’s perspectives into computer code: a boon to both usability and ease of understanding. But while objects do a good job of reflecting the structure, they fail to reflect the actions of the system, and DCI is conceived to reflect the roles and interactions between the roles in the end user’s cognitive model.

Traditionally, object-oriented programming languages have not been able to capture the collaboration between objects and reflect the algorithms that flow through the collaboration. Just as an instance of an object reflects a domain structure, the collaboration and interaction of objects are also structured. Collaboration and interaction are also part of the end user’s mental model, but you won’t find a cohesive representation of them in your code. Personas are, in essence, generalized, abstract algorithms. The avatar has no flesh and blood and can’t do any real work. After all, the work falls on the object, and the object itself is responsible for embodying the domain model. In people’s mind, there are two different models for the unified whole of “object”, namely, “what is the system” and “what does the system do”, which is the fundamental problem to be solved by DCI. The user recognizes the individual objects and the domain they represent, and each object must perform some behavior according to the interaction model in the user’s mind, connecting to other objects through the role it plays in the use case. Because the end user can combine the two perspectives, objects of a class can perform the member functions of the role they play, in addition to supporting the member functions of their own class, 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 a part of the object that is no less important than the methods we get from the class when the object is initialized. We have all the logic that an object might need to play a role laid out at compile time. We could have done this if we had been a little smarter and learned which roles were assigned at run time, and then injected the logic that happened to be needed.

The algorithm and role-object mapping are owned by the Context. Context “knows” which object in the current use case should act as the actual actor, and is then responsible for “casting” the object into the corresponding role in the scene. Another implication is associated with 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 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 the complete DCI architecture (Data, Context, and Interactive architecture) :

  1. The Data layer describes the domain concepts of the system and their relations. This layer focuses on the establishment of domain objects and the life cycle management and relations of these objects, allowing programmers to think about the system from the perspective of objects, so that “what is the system” can be understood more easily.
  2. Context layer: is the thinnest layer possible. Context is often implemented stateless, just find the appropriate roles and let them interact to complete the business logic. But simplicity does not mean insignificance, and the explicit Context layer provides the entry point and thread for understanding software business processes.
  3. The Interactive layer is mainly embodied in the modeling of roles, which are the real executor of complex business logic in each context and reflect “what the system does”. What a role does is model behavior, and it connects the context to the domain object. Since the behavior of the system is complex and changeable, role separates the stable domain model layer from the changeable system behavior layer, and role focuses on modeling the system behavior. This layer tends to focus on the scalability of the system, which is more close to software engineering practice. In object-oriented, it is more thought and design from the perspective of classes.

DCI is now widely seen as an evolution and complement to DDD for object-oriented domain modeling. Explicit modeling of role solves the contention between hyperemia model and anemia model in object-oriented modeling. DCI explicitly uses role to model behaviors, and at the same time allows role to bind to corresponding domain objects (cast) in context, so as to not only solve the problem of inconsistent data boundary and behavior boundary, but also solve the problem of high cohesion and low coupling of data and behavior in domain objects.

One of the thorny problems of object-oriented modeling is that data boundaries and behavior boundaries are often inconsistent. Following the idea of modularity, we encapsulate behavior with its tightly coupled data through classes. However, in complex business scenarios, behaviors often span multiple domain objects. If such behaviors are placed in one object, other objects must disclose their internal state to the object. So with the development of object orientation, there are two schools of conflict in domain modeling. One tends to model behavior across multiple domain objects in domain services. If this approach is overused, it can cause domain objects to become dumb objects that only provide a bunch of GET methods, a modeling result known as an anaemic model. The other school firmly believes that methods should belong to domain objects, so all business activities are still placed in domain objects, resulting in domain objects becoming God classes as more business scenarios are supported, and the level of abstraction of methods within classes is difficult to be consistent. In addition, because the behavior boundary is difficult to be appropriate, the data access relationship between objects is also complicated, and the modeling result is called congestion model.

For multi-role objects, here’s a real-life example:

People have multiple roles, and different roles perform different duties:

  1. As parents: We read stories to our children, play games with them, put them to bed.
  2. As children, we should honor our parents and listen to their life advice.
  3. As subordinates: we should obey the work arrangement of the boss and finish the task with high quality.
  4. As a boss: We should arrange the work of subordinates, and train and motivate them.
  5. .

Here, people (large objects) aggregate multiple roles (small classes), and people can only play specific roles in a certain scene:

  1. In front of our children, we are parents.
  2. In front of our parents, we are children.
  3. We are subordinates to our superiors.
  4. We are bosses in front of our subordinates.
  5. .

With the introduction of DCI, the Domain layer in DDD four-tier architecture pattern has been thinned. Previously, the Domain layer corresponds to the three layers in DCI, but now:

  1. The Domain layer retains only the Data and Interaction layers in DCI, which we typically use in practice with directory isolation, with two directories object and Role separating the Data and Interaction layers.
  2. The Context layer in DCI is moved up from the Domain layer to the Context layer.

As a result, the DDD layered architecture pattern becomes five, as shown in the following figure:

ddd-l5.png

In practice, the author defines these five layers of localization as:

  1. The User Interface is the User Interface layer. It is mainly used to process Restful requests sent by users, parse configuration files entered by users, and transfer information to the Interface of the Application layer.
  2. The Application layer is the Application layer, which is 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 and the relevant Context of the current service to process it.
  3. Context refers to the environment layer. The Domain objects of the Domain layer are cast into appropriate roles so that the roles can interact to complete the business logic.
  4. Domain layer is the Domain layer, which defines the Domain model, including not only the modeling of Domain objects and their relationships, but also the explicit modeling of the role of the object.
  5. The Infrastructure layer is the basic implementation layer that provides common technical capabilities for other layers: business platforms, programming frameworks, persistence mechanisms, messaging mechanisms, encapsulation of third-party libraries, common algorithms, and so on.

Finished discussing the DDD five-tier architecture pattern? 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 or management surface and have a lot of message interaction. A business of this kind of system contains a sequence of synchronous messages or asynchronous messages. If they are all placed in the Context layer, the code of this layer will be complicated. Therefore, we consider:

  1. The Context layer is divided into two layers, namely the Context layer and the large Context layer, in the system that faces the control plane or the management plane and has a lot of message interaction.
  2. Context layer processing unit is Action, corresponding to a synchronous or asynchronous message.
  3. The large Context layer, which corresponds to a Transaction and consists of a sequence of actions, is generally implemented through the Transaction DSL, so we tend to call the large Context layer the Transaction DSL layer.
  4. The Application layer usually does some scheduling related work in a control-or management-oriented system with a lot of message interaction, so we used to call the Application layer the Scheduler layer.

Therefore, in a control-side or management-side system with a lot of message interaction, the DDD layered architecture pattern becomes six layers, as shown in the following figure:

ddd-l6.png

In practice, the author defines these six layers of localization as:

  1. The User Interface is the User Interface layer, which is mainly used to process Restful requests sent by users, parse configuration files entered by users, and pass information to the Interface of the Scheduler layer.
  2. Scheduler is the scheduling layer, which is 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 Transaction related to the operation.
  3. Transaction is a Transaction layer that corresponds to a business process, such as UE Attach, that combines the processing sequences of multiple synchronous or asynchronous messages into a single Transaction, and in most cases, has a selection structure. In case the transaction fails, it is rolled back immediately. When the transaction layer receives a request from the scheduling layer, it delegates the Action of the Context layer to handle the request, often along with the selection of the Action using the Context layer Specification (predicate).
  4. Context is the environment layer, with Action as the unit, processing a synchronous message or asynchronous message, casting Domain objects in the Domain layer into appropriate roles, so that roles interact to complete the business logic. The environment layer usually also includes Specification implementations, which use Domain layer knowledge to make a conditional judgment.
  5. Domain layer is the Domain layer, which defines the Domain model, including not only the modeling of Domain objects and their relationships, but also the explicit modeling of the role of the object.
  6. The Infrastructure layer is the basic implementation layer that provides common technical capabilities for other layers: business platforms, programming frameworks, persistence mechanisms, messaging mechanisms, encapsulation of third-party libraries, common 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. As for the transaction model, THE author has previously shared an article – “Golang Transaction Model”, interested students can check it out.

To sum up, DDD six-tier architecture can be regarded as a variant of DDD five-tier architecture in a specific field, which is collectively called DDD five-tier architecture, and DDD five-tier architecture is similar to the traditional four-tier architecture, which is a restricted loosely layered architecture.

Pattern 3: Hexagonal architecture

One approach to improving the layered architecture is Dependency Inversion Principle (DIP), which works by changing the dependencies between different layers.

The dependency inversion principle was proposed by Robert C. Martin and formally defined as: high-level modules should not depend on low-level modules, but both should depend on abstractions. Abstraction should not depend on details, details should depend on abstractions.

According to this definition, the lower-level components in the DDD hierarchical architecture should depend on the interfaces provided by the higher-level components, that is, both the higher-level and lower-level components depend on abstractions, and the entire hierarchical architecture seems to be bulldozed. If we flatten the layered architecture and add some symmetry to it, we get an architectural style characterized by symmetry, the hexagon architecture. The hexagonal architecture was proposed by Alistair Cockburn in 2005, in which different customers interact with the system in an “equal” way. Need new customers? Not a problem. You just need to add a new adapter to convert customer input into parameters understood by the system API. At the same time, for each specific output, a newly created adapter is responsible for the transformation.

The hexagonal architecture, also known as ports and adapters, is shown below:

ddd-hex.png

Each different edge of the hexagon represents a different type of port, which handles either input or output. There is an adapter for each external type, and the external interacts with the internal through application layer apis. In the figure above, three customer requests all arrive at the same input ports (adapters A, B, and C), and another customer request uses adapter D. Assume that the first three requests use THE HTTP protocol (browser, REST, SOAP, etc.) and the last request uses the AMQP protocol (such as RabbitMQ). Ports are not clearly defined and are a very flexible concept. Regardless of how ports are divided, when a client request arrives, an adapter should translate the input, and then the port will invoke an application action or send an event to the application, giving control to the internal region. Applications receive customer requests through a common API and process the requests using a domain model. We can think of the implementation of Repository, the modeling element of DDD tactical design, as a persistence adapter that can be used to access previously stored aggregation instances or to hold new aggregation instances. As illustrated by adapters E, F, and G in the figure, resource libraries can be implemented in different ways, such as relational databases, document-based storage, distributed caching, or memory storage. If the application sends a domain event message to the outside world, we will use adapter H to process it. This adapter handles message output, whereas the aforementioned adapter 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 simultaneously. Once a component’s functionality is clear, development can begin immediately. Because the component has multiple users with different priorities, multiple different interfaces need to be provided. At the same time, the understanding of these users is also deepening, and the related interface may be refactored multiple times. As a result, multiple users of a component often discuss these issues with the developer of the component, potentially reducing the development efficiency of the component. In a different way, component developers focus on the development of features after they have identified the functionality of the component, ensuring that features are stable and efficient. The user of the component defines the interface (port) of the component, writes tests based on the interface, and evolves the interface over time. When cross-tier integration testing is done, a component developer or user can develop another adapter.

The evolution of the hexagonal architecture pattern

As good as the hexagonal architecture is, there is no best, there is no end to evolution. In the years since the hexagon schema was proposed, three variations of the hexagon schema have been developed, which interested readers can follow the link to learn for themselves:

  1. The Hexagonal architecture is a superset of the Onion architecture proposed by Jeffrey Palermo in 2008.
  2. Robert C. Martin proposed Clean Architecture in 2012, which is a variant of the hexagonal Architecture.
  3. Russ Miles proposed the Life Preserver design in 2013, which is based on a hexagonal architecture.

summary

This paper firstly reviews DDD and hierarchical architecture with readers, and then elaborates three commonly used DDD hierarchical architecture patterns (four-tier architecture, five-tier architecture and hexagonal architecture) with practical experience, so that readers can have a deep understanding of DDD hierarchical architecture pattern. In order to choose the most appropriate DDD layered architecture mode according to the specific situation in microservices development practice, so as to deliver clear structure and easy to maintain software products.