Each technology and architecture has its own historical background and evolution; Similarly, each technology and architecture has its own strengths, weaknesses and business scenarios to suit. Therefore, this paper briefly analyzes the emergence background and evolution of DDD from the perspectives of “hyperemia model” and “hierarchical architecture evolution”. “I know what you said, show me your model, show me you code”. For how to design DDD and how to develop DDD, the following part of this article also takes a business example to communicate and discuss with everyone.
Hyperemic domain model VS anaemic model
Anemic model and bulky services
Anemia model refers to the model of anemia of its domain object. In the anaemic model, domain objects are only used as data carriers, with no behavior or business logic, which is typically placed in services, utils, helpers, and so on. This design (the three layers shown on the left) is the most common development pattern today.
When we implement specific functionality, we first discuss the classes that will be persisted, which we call entities. They are represented by the letter E in the diagram above. These entities are actually object-oriented representations of database tables. We didn’t implement any business logic inside them. Their only role is to be mapped to their database equivalents by some ORM.
When our mapped entities are ready, the next step is to create classes to read and write them. DAO(Data Access Object). Typically, each of our entities represents a different business use case, so the number of DAO classes matches the number of entities. They do not contain any business logic. The DAO class is nothing more than a tool for retrieving and persisting entities. We can create them using existing frameworks, such as Hibernate.
The final layer on top of the DAO is the essence of our implementation, Service. Services utilize DAOs to implement business logic for specific functions. Regardless of the number of capabilities, a typical service always does the following: load entities using daOs, modify their state as required, and persist them. Martin Fowler describes this architecture as a series of transaction scripts. The more complex the functionality, the greater the number of operations between load and persistence. Often, some Servcie will use other Servcie, resulting in increased code volume and complexity.
Rich model and thin services
Domain-driven design offers a completely different approach to code layering. The common architecture of the domain-driven design model is shown on the right.
Note the Service layer in DDD, which is much thinner than the Service layer in the anaemic model. The reason is that most of the business logic is contained in aggregate, entity, and value objects.
The Domain layer has more types of objects. There are value objects, entities, and objects that make up an Aggragate. Aggragate can only be connected by an identifier. They cannot share any other data with each other.
The final layer is also thinner than the previous one. The number of repositories corresponds to the number of aggregations in DDD, so an application using the anaemic model has more repositories than an application using the rich model.
Easier changes and less bugs?
Application domain-driven design architectures offer some important benefits to developers.
The anemia model is actually one kind of Procedural programming. Although it is relatively straightforward, it is difficult for us to know the process of the object changing (why it should change, what the condition is, how it becomes and what it becomes). Because the business logic resides in services, which are stateless (holding no object state), it is difficult to understand how object state changes, especially when services call each other. At the same time, services hold a large number of object state handling logic, and there is a lot of repetitive code.
By dividing objects on entity and value objects, we can manage newly created objects in our application more precisely. The existence of aggregation makes our API simpler, and changes within the aggregation are easier to implement (we don’t need to analyze the impact of our changes on the rest of the Domain).
At the same time, fewer connections between Dmoain elements reduce the risk of errors when developing and modifying code. Avoid consistency issues and so on.
Reference: Domain-driven Design vs. Anemic Model. How do they differ?
Domain-driven architecture
The most classic, and most commonly used, is the three-tier model. The three-layer pattern is divided into UI layer, Service layer, and Dao layer. In this architecture, business logic is defined in Service objects in the business logic layer, and domain objects that reflect domain concepts are defined as Java Beans, which do not contain any domain logic and are therefore placed in the data access layer. The architecture diagram of the three-tier mode is as follows:
As mentioned above, this traditional 30% model leads to an anemic model. To avoid anemic Model, it is necessary to allocate the behavior of manipulating data reasonably to Domain Model (Entity and Value Object in tactical design) instead of Service in three-tier Model. Then the architecture should be as follows:
In the above Model, the behavior of the business is reasonably allocated to the Domain Model objects, which can avoid anaemia and at the same time does not cause too much bloat in the business logic layer.
After the development of a system, it is impossible to remain unchanged. It will be constantly updated and iterated, so the requirements will be constantly updated and changed. But, on closer inspection, there are always signs of change. First, the change of user experience and operating habits will often lead to the changeful display of the system interface; Secondly, the change of deployment platform and component switch will often lead to the changeability of the underlying storage of the system. In general, however, the core domain logic of the system is basically unchanged, regardless of how the system’s foreground presentation and underlying storage changes.
In the above architecture, the domain layer relies on the infrastructure layer, and this coupling makes the domain layer vulnerable, so we need to find ways to make the domain layer more pure and stable. The method is very simple, and that is to rely on abstraction, not concrete. Therefore, the system hierarchy can be redesigned. The new design is as follows:
In this layered pattern, domain objects are persisted through abstracted interfaces at the infrastructure layer. But is it appropriate to add a new “infrastructure layer abstraction”?
From a business perspective, the life cycle of managed objects is a must, but access to external resources is not. It is only because computer resources are not sufficient for this stability that external resources have to be brought in. That is, accessing these domain objects is a business element, and how they are accessed, such as through external resources, is a technical element of concrete implementation.
From a coding point of view, a domain object instance is nothing more than a data structure, the only difference being where it is stored. Domain-driven design abstracts the data structures that manage these objects into repositories. Accessing domain objects through this abstract repository should naturally be considered a domain behavior. If the repository is implemented as a database and life cycle management of domain objects is implemented through the mechanism of database persistence, then this persistence behavior is a technical factor.
Therefore, the Domain level should be relocated to the Domain level. The domain level in the figure is no longer dependent on any other components or classes, and becomes a pure domain model. The new architecture is as follows:
The above architecture is the final version of DDD’s four-tier architecture. The criticism of each layer is as follows:
Show the layer
It is responsible for displaying information to users and interpreting user commands to complete the front-end interface logic. The user here does not have to be the person using the user interface, but could be another computer system.
Application layer It is a very thin layer, responsible for the coordination between the presentation layer and the domain layer, is also a necessary channel for interaction with other system application layer. It is mainly responsible for the composition, orchestration and forwarding of services, the execution sequence of business use cases and the assembly of results. After assembling domain services, they are published as coarse-grained services through THE API gateway to front-office applications. In this way, the complexity of the domain layer and its internal implementation mechanisms are hidden. In addition to defining application services, the application layer can also perform security authentication, permission verification, persistent transaction control or send event-based message notifications to other systems.
The code of this layer mainly calls domain layer services to complete service composition and orchestration to form coarse-grained services and provide API services for the foreground. This layer of code can perform business logic data verification, authority authentication, service composition and orchestration, distributed transaction management and other work.
Domain layer is the core of business software. It contains domain objects (entities, value objects), domain services and their relationships, and is responsible for expressing business concepts, business state information and business rules, in the form of domain model. Domain-driven design advocates a rich domain model, in which business logic is ascribed to domain objects as much as possible, and the parts that really cannot be ascribed are defined as domain services.
This layer of code mainly realizes the core business domain logic, and it is necessary to do a good job of stratification of domain code and logical isolation of code between aggregations
Infrastructure Layer The foundation of a system is not limited to database access, but also to access such hardware facilities as networks, files, message queues, and so the name of this layer “infrastructure layer” makes sense. It provides common technical capabilities to other layers, delivers messages to the application layer (API gateways, etc.), provides persistence mechanisms to the domain layer (database resources, etc.), and so on.
Based on the principle of dependency inversion, basic resource services are encapsulated to realize invocation dependency inversion between resource layer and application layer and domain layer. Basic resource services (such as database and cache) are provided for application layer and domain layer to decouple each layer and reduce the impact of changes in external resources on core business logic.
This layer mainly includes two types of adaptation code: active adaptation and passive adaptation. Active adaptation code mainly provides API gateway service for front-end applications, and performs simple front-end data verification, protocol and format conversion adaptation, etc. Passive adaptation mainly aims at back-end basic resources (such as databases and caches), and provides data persistence and data access support through dependency reversal at the application layer and domain layer to decoupled the resource layer.
Hexagonal structure
In the four-tier DDD architecture, the principle of dependency inversion is adopted. The domain layer achieves decoupling by relying on abstract interfaces. The same idea, if we apply the dependency principle to the application layer, we can actually decouple it. In fact, it can be found that when the principle of dependency inversion is applied to all layers in a hierarchical architecture, there is no concept of hierarchical architecture. Both the high-level and the low-level are dependent on abstraction, which seems to push the whole hierarchical architecture flat, thus evolving the hexagonal architecture of DDD:
The hexagon architecture divides the system into two layers: internal and external hexagon. The internal hexagon represents the core business logic of the application, and the external hexagon represents the external application, drivers and basic resources. The internal communicates with the external through ports and adapters, providing services in the form of API active adaptation, and presenting resources in the form of passive adaptation through dependency inversion. A port may correspond to multiple external systems, and different external systems use different adapters that convert protocols. This enables applications to be driven in a consistent manner by users, programs, automated tests, and batch scripts.
Dispel the concept of domain driven
DDD design includes strategic design and tactical design. In the strategic design stage, domain modeling and service map are mainly completed. In the tactical design stage, the construction and implementation of microservices are completed through aggregation, entities, value objects and services at different levels. DDD ensures consistency between business models, system models, architectural models, and code models.
1. What are domain services?
When an operation or transformation process in a domain is not the responsibility of an entity or value object, the operation should be placed in a separate interface, the domain service. Domain services, like other domain models, focus on the business of a particular domain.
Are domain services different from application services?
Answer: From the criticism, application services do not deal with business logic, but domain services do; In terms of relative roles, application services are the clients of domain services.
Does the domain service operate on multiple domain objects including multiple aggregations?
A: It is possible for a domain service to process multiple domain objects in a single atomic operation.
2. What does Repository do? Relationship with DAO?
Daos are primarily viewed from a database table perspective. They operate on DO classes and provide CRUD operations, a data-oriented style (transaction scripting).
Repository corresponds to the abstraction of Entity object reading and storing, which is unified at the interface level and does not care about the underlying implementation. For example, save an Entity object, but whether it is an Insert or an Update doesn’t matter. The Repository implementation class calls daOs to perform operations and converts AccountDO to Account through Builder/Factory objects.
3, what is the anticorrosion layer, what is the role?
This article for the anti-corrosion Layer (ACL) definition is very good, as follows:
Most of the time our system will rely on other systems, and the dependent system may contain unreasonable data structure, API, protocol or technical implementation, if the strong dependence on external systems, will lead to the “corrosion” of our system. At this point, by adding an anticorrosion layer between the systems, it can effectively separate the external dependencies from the internal logic, so that no matter how the external changes, the internal code can remain unchanged as much as possible.
Acls are more than just a layer of calls. In practice, ACLs can provide more powerful functions:
- Adapter: In most cases, external data, interfaces, and protocols do not conform to internal specifications. The adapter mode encapsulates data conversion logic inside acLs, reducing intrusion into service codes. In this case, we convert each other’s incoming and outgoing parameters by encapsulating ExchangeRate and Currency objects to make the incoming and outgoing parameters more appropriate to our standards.
- Cache: For external dependencies that are frequently invoked and data changes are infrequent, embedding cache logic in acLs can effectively reduce the request pressure for external dependencies. At the same time, caching logic is often written in business code, so embedding caching logic in ACLs can reduce the complexity of business code.
- Backpocket: If the stability of external dependencies is poor, an effective strategy to improve the stability of our system is to use acLs to play the role of backpocket, such as returning the last successful cache or business backpocket data when the external dependencies fail. This back-pocket logic is generally complex and difficult to maintain if scattered in the core business code. By concentrating in acLs, it is easier to test and modify.
- Easy to test: Similar to the previous Repository, the ACL interface class can be easily implemented as a Mock or Stub for unit testing.
- Function switch: Sometimes we want to enable or disable the function of an interface in certain scenarios, or make an interface return a specific value. We can configure the function switch in the ACL to achieve this without affecting the real business code. Also, using functional switches makes it easy to implement Monkey tests without actually physically turning off external dependencies.
A refactoring of traditional development patterns
Ali technical experts explain DDD series second bomb – application architecture, this article through a transfer case is a good explanation of the traditional development model may exist problems, as well as the reconstruction scheme, interested can go to see, I here simply elaborate on its main ideas and content.
Possible problems
For a complex business, traditional three-tier development may have the following problems:
Poor maintainability
- Data structure instability: The DO class is a pure data structure that maps a table in the database. The problem here is that the table structure and design of the database are external dependencies of the application and may change in the long run, for example, the database needs to do Sharding, or change the table design, or change the field name.
- Uncertainty of dependence of third-party services: changes of third-party services: API signature changes, or service becomes unavailable. Alternative services need to be found. In these cases, retrofit and migration costs are significant. At the same time, external dependence on the bottom, limiting current, circuit breaker and other schemes need to be changed.
- Middleware change: Today we use Kafka to send messages, tomorrow we need to use RocketMQ on Alibaba Cloud? What happens the day after tomorrow if the message serialization is changed from String to Binary? What if I need message sharding?
Poor scalability
Extensibility = how much code needs to be added/modified to make new requirements or logic changes
- Business logic cannot be reused: Core business logic cannot be reused due to incompatible data formats. The consequence of having special logic for each use case is that you end up with a lot of if-else statements, and this branching logic makes parsing code very difficult, making it easy to miss boundary cases and cause bugs.
- Logic and data store interdependence: As the business logic grows more complex, it is likely that the added logic will require changes to the database schema or message format. Changing the format of the data causes other logic to move along with it. In the most extreme scenarios, the addition of a new feature can lead to the refactoring of all existing features at great cost.
Poor testability performance
Testability = time spent running each test case * number of additional test cases per requirement
- Setup difficulty: When code is heavily dependent on external dependencies such as databases, third party services, middleware, etc., running through a test case requires ensuring that all dependencies work, which can be extremely difficult early in the project. In the later stage of the project, the test will fail due to the instability of various systems.
- Long run time: Most external dependency calls are I/O intensive, such as cross-network calls, disk calls, and so on, and these I/O calls take a long time to test. Another common reliance is on cumbersome frameworks such as Spring, which often takes a long time to start. When a test case takes more than 10 seconds to run, most development doesn’t test very often.
- High degree of coupling: If there are three sub-steps A, B and C in A script, and each step has N possible states, when the coupling degree of multiple sub-steps is high, in order to completely cover all use cases, A maximum of N N test cases are required. As more substeps are coupled, the number of test cases required grows exponentially.
Refactoring principle
Software design generally follows the following principles:
- The Single Responsibility Principle: The Single Responsibility Principle states that an object/class should have only one reason for change. But in this case, the code could change because of any change in external dependencies or computational logic.
- The Dependency Inversion Principle: The Dependency Inversion Principle requires that abstractions, not concrete implementations, be relied on in your code. In this case, the external dependencies are implementation-specific. For example, YahooForexService is an interface class, but it relies on a specific service provided by Yahoo, so it is implementation-dependent. The same KafkaTemplate, MyBatis DAO implementation are concrete implementation.
- The Open Closed Principle: The Open Closed Principle means that extensions are Open, but modifications are Closed. The calculation in this case is code that could be modified, and in this case the logic would need to be wrapped as an unmodifiable calculation class, with the new functionality implemented through an extension of the calculation class.
Reconstruction scheme
Step 1: Abstract the data storage layer.
The Data Access layer is abstracted to reduce the system’s direct dependence on the database. The general method is as follows: 1) Create an Entity object: An Entity is a domain object with an ID, except that it has data. Entity is independent of the database storage format and is designed according to the Ubiquitous Language of the field. AccountRepository: Repository is responsible for storing and reading Entity objects. The Repository implementation is responsible for storing and reading Entity objects. By adding the Repository interface, the underlying database connections can be replaced with different implementation classes.
Step 2: Abstract third-party services
Similar to database abstraction, all third-party services need to be abstracted to solve the problem of uncontrollable third-party services and strong coupling of input and output parameters. This step usually means setting up a coating ACL.
Step 3: Encapsulate the business logic
Change business logic from scattered in servies to encapsulated in Domain Entities and Domain Services.
Implementation of domain driver landing
This article chooses a relatively simple business scenario to illustrate, because the business is too complex to be clear at a time, and people also struggle to understand. The demand scenario is to build a data flow service integrating merchant management and demand management to help us manage and track production. The specific items are as follows:
1) Multiple requirements can be built under each merchant; 2) Multiple orders will be generated after the requirements are confirmed; 3) For each order, multiple deliveries will be made; 4) Each delivery needs to accept whether it is passed or not. 5) Requirements, delivery and acceptance can be explained by attachment if necessary (attachment information field is not modified, if modified, it will be uploaded to the new one);
Process decomposition + object modeling
Process decomposition is to split the demand into steps. Object modeling, is to split the requirements, extract objects and operations, object modeling.
Process decomposition is usually top-down, from a large requirement, gradually broken down; And object modeling, generally from bottom to top, for each small Step modeling and object analysis. Therefore, the whole process is a combination of top and bottom, complementing each other. The above analysis can help us better clarify the relationship between models, while the following model expression can improve the reuse degree of our code and business semantic expression ability.
How to write complex business code
How to identify domain objects, aggregates, entities, and value objects
Whether multiple objects should be used as an aggregation is determined by:
- Name correlation
- Periodicity of life
- Strong consistent correlation
- Memory overhead and complexity
Generally, an aggregation is determined by the above criteria. First, the requirements are split and finally divided into multiple operations. If multiple operations are name-dependent, that is, all related to a domain object, then that domain object may be a waiting aggregation. Complex business logic does this very well, but simple business logic does not need this step because it is easy to know about domain objects.
As to which entities contained in one of the polymerization, and whether the entity should go out alone as a new polymerization of standards, the first of all we need to focus on the objects involved life periodicity, whether the life cycle is linked to (leave dependent objects, lost existence), if it is then suggested that put together, Not just by looking at other criteria.
For aggregation, there is a very good feature, that is, aggregation is a whole, and operations are all based on an aggregation. In this way, strong consistency can be guaranteed within aggregation. Therefore, if strong consistency of domain objects is required, it is recommended to be taken as an aggregation, otherwise it is judged by referring to other standards.
Aggregation is a whole, which can guarantee strong consistency, so is large aggregation good? It’s not that a large aggregation is expensive in memory; At the same time, it needs to ensure the ultimate consistency at the same time, so the concurrency is reduced, the performance will be affected.
In terms of memory overhead, the actual memory overhead is mostly caused by entities, because entities are subject to modification, so every time we load the aggregate, we have to load the entities out, and if there are a lot of entities, then the memory overhead is very high; Value objects, however, are not modified, so we should not load them in their entirety. Therefore, value objects are recommended in preference.
So here, let’s also talk about, in an aggregate, which of the pairs in the aggregate should be entities and which should be value objects? I think the most important thing is whether the object is mutable, and if mutable then it needs to be an entity, and if immutable then it needs to be a value object.
Based on the above criteria, my final design of demand management is as follows:
Code development
The DDD architecture adopted by the system is the four-tier architecture mentioned above:
Code directory structure:
How to develop EventSourcing using GO language
Afterword.
This article is based on their own actual DDD development some thoughts, while reading a large number of related articles on the Internet to do a summary, I hope to help you!