background
Why develop a reference engineering architecture
The application architecture styles adopted by different teams to implement DDD may be different, and there is no unified and standard DDD engineering architecture. Some teams may follow the classic DDD four-tier architecture, or the improved DDD four-tier architecture, some teams may comprehensively consider various architectural styles such as layered architecture, neat architecture, and hexagonal architecture, and some may introduce CQRS in practice to solve the problem of reading The difference between model and write model and so on. Even if it is impossible to formulate a common, standard engineering application architecture, it is still valuable to develop a reference architecture that follows the domain-driven design idea for the team . For the following reasons:
- Provide a quick-start engineering reference for the tactical design of the team to practice DDD
- Refer to a large number of naming and structural decisions of the project, explicitly reflect the relevant concepts of DDD, and help the team to reach a consensus on the tactical implementation of DDD
- At the same time, the reference architecture helps to precipitate some of the team's thoughts and best practices on domain-driven design
Reference Architecture Considerations
Although it is impossible to formulate a completely general DDD reference architecture, it is feasible and practical to formulate a reference architecture in a specific context . The choice of context should be as close as possible to the actual engineering practice scenario and consider multi-dimensional factors.
The reference engineering architecture described in this article adheres to the following principles:
- Follow the essential idea of domain-driven design
- Fully consider the characteristics of business system construction
- Minimize dependencies and keep them lightweight
It is hoped that the engineering reference architecture will cover the following areas
- Separation of business and technical domains
The reference architecture should follow the characteristics of technology and business isolation, and you can refer to various architectural styles. The separation of business and technical concerns is not a unique feature of DDD. This important principle is followed in the hexagonal, clean architecture, and onion architectures.
- Multiple Bounded Context Scenarios
When most teams split microservices based on DDD, especially in the initial stage of system construction, the granularity of the bounded context within a single microservice application needs to be weighed. Due to team organizational structure factors and microservice cost issues, a single application generally accommodates multiple bounded contexts (ideally 1:1). These Bounded Contexts are likely to be migrated to standalone applications with subsequent incremental iterations. Therefore, the reference architecture takes multi-context application scenarios as an important consideration.
- Clear components, responsibility boundaries and dependencies
- Support domain report scenarios: report scenarios are common in business systems, and DDD does not reflect the processing method of this scenario. As an engineering reference architecture, I still hope to start from the actual business and reflect the display support for the writing model and report model
- Minimize external dependencies: need to eliminate unnecessary dependencies and keep the engineering architecture lightweight
Anatomy of a Reference Architecture
App's multi-context structure
Based on the above principles, the reference project considers the scenario of multiple contexts in a single application, in order to make a trade-off between modularity, service granularity, and cost . The schematic diagram of application architecture supporting multiple contexts is shown below. After the solution domain identifies and divides bounded contexts, multiple contexts are implemented in a single engineering application based on their business cohesion and relevance. There may be interactions between multiple bounded contexts in a single application, and the form of interaction can be based on event-driven or in-process calls . The coupling between contexts is lower in the event-driven approach, but generally requires the introduction of event bus support, and the introduction of additional components will inevitably lead to an increase in complexity. In-process calls will have higher coupling, but the complexity will be lower from the perspective of implementation. Which way to choose specifically, developers can make trade-offs based on the actual situation.
It needs to be explained again that this application architecture decision is a multi-factor trade-off , and it is not consistent with the ideal practice of 1:1 subdomain and bounded context.
From the logical schematic diagram above, let’s go one level deeper and analyze the presentation form of the detailed application architecture from the layered dimension, as shown in the following figure:
layered concerns
client
The client is in a different process from the application, and is the consumer of application capabilities. In actual projects, it may be the APP, PC, applet, official account, or third-party business caller.
access layer
The access layer is the middle layer between the external system and the internal business capabilities of the application. The access layer is the external facade of the application layer and the entrance for the current application to expose the business capabilities to the outside world . The composition of this layer may be the HTTP interface declaration provided externally, distributed timing task scheduling, message listener, RPC service and so on. Its important responsibilities include basic parameter verification, input parameter adaptation and service routing (forwarding to the application service at the first layer) and response data adaptation for requests from external systems .
Business Layer:
This layer is the layer where the application's business logic resides. The entire architectural style adopts a modular single style, and different bounded contexts in this layer are embodied as different modules. A layered architecture is adopted in each bounded context, which is independently divided into application layer, domain layer and gateway layer .
Application layer:
Coordinate domain objects, domain services, or external dependent services to complete business use cases. This layer only coordinates capabilities and does not process any domain logic .
domain layer:
The domain layer is the core of the entire layering and has nothing to do with technical implementation. It is mainly responsible for domain models, domain events, domain service definitions, and interface abstraction of business-related external services and warehouse interfaces.
The essential difference between the domain layer and application services is that the application layer does not contain domain logic, and all domain logic is lowered to the domain layer for implementation.
Gateway layer:
Gateway layer positioning is the egress gateway of the application, an anti-corrosion layer for the interaction between the application and the external infrastructure, and handles all technical related implementations .
There are many ways to name this component. For example, some teams name it " rpc ", and some teams name it " infrastructure ". Different naming reflects the team's choice of the metaphor behind it. In the reference architecture of this article, the name gateway-Gateway was chosen. The reason for the decision is: the bounded context itself is highly cohesive, and its interaction with the outside requires a unified exit. The meaning of the gateway expressed by Gateway properly expresses this The concept of unified export. If the Facade layer is the northbound gateway of the application, it is the entrance for external system requests to enter the interior. The Gateway at this time expresses the southbound gateway of the bounded context, which is the exit for internal applications to connect to the outside.
Components and dependencies
From the macro layering, let's go deeper and look at the component division of each layer. As shown below:
Start component:
The startup entry of the entire application, loading application configuration information, and so on.
Common components
Provides the abstraction of domain model elements that are reused between different bounded contexts, such as the general abstraction of Command, Query, Event, Entity, and ValueObject. Of course, the general abstraction of the domain model does not have to be reused in the Common component. It can also be used as an independent bounded context and shared with other contexts by sharing the kernel, or it can also be implemented as an independent jar package component.
API components
The interface declaration component of the RPC type service, taking the JSF used inside the company as an example, this component is the component that applies the JSF API exposed to the external system. This component can be an independent project, of course, some teams will put it into the application project as a Module.
Unified Facade Component: Facade
The entrance of the external client to the application system is also the unified facade of the internal application service , similar to the adapter in the hexagonal architecture style. Based on different scenarios, the reference architecture is divided into several subpackages such as provider (RPC service), task (scheduled task), listener (MQ monitoring), rest (http interface), etc. After the external request enters the system, the Facade component completes operations such as basic verification of input parameters, conversion of input parameters, service routing, and conversion of output parameters. In addition, it can also undertake related capabilities such as processing login status, authentication, and logs.
Application Service Component: Application Service
Application services represent use cases and system behaviors. They complete the application logic processing of use cases by delegating to the domain layer and infrastructure layer (the Gateway component in the reference architecture). It can be understood that application services are clients of the domain layer . Typical responsibilities of this component:
Load domain objects from the storage layer, delegate domain objects to execute domain logic, and save domain objects
- Notification of important events to the outside
- Input and output parameters conversion and adaptation
- transaction processing external
- Service calls for non-domain logic
External API
The logic of application services not only needs to coordinate the domain layer, but sometimes also needs to rely on external three-party services . The External API component is responsible for the interface declaration and definition of these external services, without specific implementation.
Application service components do not directly depend on the implementation of these external services, but on their interface abstractions. At the same time, the model definition here is based on the semantics of the bounded context, which is an adaptation to the external model.
This component does not depend on other components, and is only depended on by application service components and Gateway components. The gateway component relies on the interface declaration of the External API component and provides the underlying technology implementation, and the application service component relies on its interface, and injects the specific implementation through the IOC method to complete the service call.
Note that the services that this component depends on do not involve domain logic , but are only used to support the orchestration of application services. If domain logic is involved, the interface definitions that depend on external services need to be lowered to the Domain layer.
Query
The Query component solves domain-related report query scenarios and exists as a component equivalent to the application service in the bounded context. The two components are responsible for business query and command logic respectively.
Although the Query component was introduced to provide support for report scenarios, the CQRS model was not fully introduced. In many materials, the probability of CQRS and DDD being mentioned at the same time is relatively high, because under DDD, we have solved the complex domain-oriented writing model, but in the reporting scenario, this rich domain model may not be the best choice . If both the read side and the write side are based on a unified domain model, it will generally lead to a compromise design of the domain model . In order to meet the requirements of the query side, the domain model has to introduce additional, domain-independent attributes, thus causing pollution of the domain model.
Domain
The Domain component is the core of the domain logic and is responsible for the realization of the domain logic of the entire system. It defines the abstraction of the domain model, domain services, domain events, and storage layer. This component does not depend on other components (except the common domain model abstraction component Common.
The reference architecture embodied in the above figure uses the classic modeling elements of DDD's tactical design, such as aggregation, entity, value object, storage, factory and domain event. In the actual implementation process, the abstraction of these design elements has certain challenges. The design process needs to go through continuous analysis, trade-offs and reconstruction to complete the modeling, which is where the core design lies.
Gateway
The gateway layer is responsible for the realization of the entire technical correlation and is the egress gateway of internal applications.
Technical dependency is the fundamental feature that distinguishes gateway components from other components. All details of technical implementation should be handled in this component, such as interaction with external services, middleware, DB, etc. At the same time, it cooperates with the interface abstraction of Domain components and External API components, and jointly undertakes the anti-corrosion layer function between the system and external dependencies (including external services and application-dependent middleware, DB and other infrastructure), and is responsible for internal models to external models. Transformation, external model to internal model conversion, and specific interactions. Based on the characteristics of the gateway component, it is also very suitable for unified external service data caching and downgrade fuse processing at this layer.
The gateway layer depends on the Domain, Application Service, External API and Query components, and is responsible for the implementation of the interfaces defined by the above four components. In the Gateway component, the implementation is isolated through subpackages:
- query: query the implementation of the service component
- external: Interface implementations in External API components that depend on external services
- repository: the implementation of the storage interface
at last
The choice of application architecture pattern is one of the important dimensions of system architecture design. The structure is not just a simple package structure and naming, it conveys a top-level abstraction, which contains a lot of practice and knowledge . It is very important to develop an engineering reference architecture that is appropriate for the team and to achieve consensus among team members. Domain-driven design does not have a unified, general-purpose architecture, and it is impractical to try to define a standard architecture. The engineering architecture described in this article is just a reference. In practice, it should be different based on the specific situation of the team, but in principle, it should follow the core concept of separating the business domain from the technical domain .
Author: Ni Xinming, Jingdong Technology
Content source: JD Cloud developer community