DDD layered architecture best practices

When it is still in a single application, it is a layered architecture. We use the three-tier architecture the most. Now it is the era of microservices. There are several commonly used microservice architecture models, such as: clean architecture, CQRS (command query separation), and hexagonal architecture. Each architecture model has its own application scenario, but its core is the principle of " high cohesion and low coupling ". Using the concept of domain-driven design (DDD) to cope with the impact of daily accelerated business changes on the architecture, the boundaries of the architecture are becoming clearer and each performs its own duties, which is also in line with the design philosophy of microservice architecture. The layered architecture based on the concept of Domain Driven Design (DDD) has become the best practice method for microservice architecture practice.

1. What is DDD layered architecture

1. Traditional three-tier architecture

To understand the layered architecture of DDD, first understand the traditional three-tier architecture.

Traditional three-tier architecture process:

  • The first step is to consider the database design, how to build the data table, how to design the relationship between the tables
  • The second step is to build a data access layer, such as choosing an ORM framework or splicing SQL operations
  • The third step is the realization of business logic. Since we designed the database first, our whole thinking will revolve around the database, thinking about how to write data in order to correctly write the data into the database. At this time, the standard practice of CRUD appears. There is not much consideration about object-oriented and decoupling. Such code is naturally more and more difficult for daily maintenance
  • The fourth step represents the output of the layer mainly for users

2. DDD layered architecture

In order to solve the high coupling problem and easily cope with future system changes, we proposed the concept of using domain-driven design to design the architecture.

This paragraph partly summarizes the thoughts after reading "07 | DDD Layered Architecture: Effectively Reduce the Dependence between Layers" from Ou Chuangxin's "DDD Practice Course"

1) Domain layer

First of all, let's put aside the trouble of the database, start with the business logic , and no longer consider the implementation of the database when designing. Split the previous business logic layer (BLL) into a domain layer and an application layer.

The domain layer focuses on the realization of business logic of business objects, reflecting the logic changes of real-world business. It is used to express business concepts, business status and business rules. For business analysis, you can refer to: "Analyze business using domain-driven design"

2) Application layer

The application layer is the upper layer of the domain layer, relying on the domain layer, is the coordination and orchestration of aggregations, and in principle does not include any business logic. It provides support for the front-end interface with a coarser-grained closure. In addition to providing upper-level calls, it can also include event and message subscription.

3) User interface layer

The user interface layer faces the data ingress interface for user access, and can provide different user interface implementations according to different scenarios. Web-oriented services can be provided in http restful mode, which can add functions such as security authentication, permission verification, and logging; micro-service-oriented services can be provided in RPC mode, and functions such as current limiting and fusing can be added.

4) Infrastructure layer

The infrastructure layer is the data outbound interface, encapsulating the technical details of data invocation. It can provide services for any other layer, but in order to solve the coupling problem, the principle of dependency inversion is adopted. The other layers only rely on the interface of the infrastructure and are separated from the specific implementation.

Two, DDD layered code implementation

1. Structural model

2. Directory structure

.
├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │   └── fun
    │   │       └── barryhome
    │   │           └── ddd
    │   │               ├── WalletApplication.java
    │   │               ├── application
    │   │               │   ├── TradeEventProcessor.java
    │   │               │   ├── TradeMQReceiver.java
    │   │               │   └── TradeManager.java
    │   │               ├── constant
    │   │               │   └── MessageConstant.java
    │   │               ├── controller
    │   │               │   ├── TradeController.java
    │   │               │   ├── WalletController.java
    │   │               │   └── dto
    │   │               │       └── TradeDTO.java
    │   │               ├── domain
    │   │               │   ├── TradeService.java
    │   │               │   ├── TradeServiceImpl.java
    │   │               │   ├── enums
    │   │               │   │   ├── InOutFlag.java
    │   │               │   │   ├── TradeStatus.java
    │   │               │   │   ├── TradeType.java
    │   │               │   │   └── WalletStatus.java
    │   │               │   ├── event
    │   │               │   │   └── TradeEvent.java
    │   │               │   ├── model
    │   │               │   │   ├── BaseEntity.java
    │   │               │   │   ├── TradeRecord.java
    │   │               │   │   └── Wallet.java
    │   │               │   └── repository
    │   │               │       ├── TradeRepository.java
    │   │               │       └── WalletRepository.java
    │   │               └── infrastructure
    │   │                   ├── TradeRepositoryImpl.java
    │   │                   ├── WalletRepositoryImpl.java
    │   │                   ├── cache
    │   │                   │   └── Redis.java
    │   │                   ├── client
    │   │                   │   ├── AuthFeignClient.java
    │   │                   │   └── LocalAuthClient.java
    │   │                   ├── jpa
    │   │                   │   ├── JpaTradeRepository.java
    │   │                   │   └── JpaWalletRepository.java
    │   │                   └── mq
    │   │                       └── RabbitMQSender.java
    │   └── resources
    │       ├── application.properties
    │       └── rabbitmq-spring.xml
    └── test
        └── java



This structure is a simple structure of a single microservice, with all layers in the same module.

In the process of large-scale project development, in order to achieve the authority control of the core module or better flexibility, the structure can be adjusted appropriately, please refer to the "Digital Wallet System" system structure

3. Realization of the domain layer (domain)

After business analysis ("Analysis of Business Using Domain-Driven Design"), start to write code. The first is to write the domain layer, create domain objects and domain service interfaces

1) Domain object

The domain objects here include entity objects and value objects.

Entity object : an object that has a unique identifier, can exist alone and can be changed

Value object : an object that cannot exist alone or exists alone at the logical level, meaningless and immutable

Aggregation : a collection of multiple objects, externally as a whole

Aggregation root : the entity object in the aggregation that can represent the entire business operation, through which it provides external access operations, it maintains the data consistency within the aggregation, and it is the manager of the objects in the aggregation

Code example:

// 交易
public class TradeRecord extends BaseEntity {
    /**
     * 交易号
     */
    @Column(unique = true)
    private String tradeNumber;
    /**
     * 交易金额
     */
    private BigDecimal tradeAmount;
    /**
     * 交易类型
     */
    @Enumerated(EnumType.STRING)
    private TradeType tradeType;
    /**
     * 交易余额
     */
    private BigDecimal balance;
    /**
     * 钱包
     */
    @ManyToOne
    private Wallet wallet;

    /**
     * 交易状态
     */
    @Enumerated(EnumType.STRING)
    private TradeStatus tradeStatus;

  	@DomainEvents
    public List<Object> domainEvents() {
        return Collections.singletonList(new TradeEvent(this));
    }
}

// 钱包
public class Wallet extends BaseEntity {

    /**
     * 钱包ID
     */
    @Id
    private String walletId;
    /**
     * 密码
     */
    private String password;
    /**
     * 状态
     */
    @Enumerated(EnumType.STRING)
    private WalletStatus walletStatus = WalletStatus.AVAILABLE;
    /**
     * 用户Id
     */
    private Integer userId;
    /**
     * 余额
     */
    private BigDecimal balance = BigDecimal.ZERO;

}



From the system design of the wallet transaction example, any operation of the wallet, such as recharge, message, etc., drives the change of the wallet balance through the transaction object

  • The transaction object and the wallet object are both entity objects and form an aggregation relationship. The transaction object is the aggregation root of the wallet transaction business model, representing the aggregation to provide external calling services
  • After analyzing the one-to-many relationship between the transaction object and the wallet object (@ManyToOne), JPA is used as the ORM architecture. For more JPA practices, please refer to >>

The domain modeling here uses an anemia model, which has a simple structure, single responsibilities, good isolation, but lacks object-oriented design ideas. For domain modeling, please refer to "Anemia Model and Congestion Model for Domain Modeling"

  • domainEvents() is an implementation of domain event publishing. The function is that any data operation of the transaction object will trigger the publishing of the event, and then cooperate with the event subscription to implement the event-driven design model. Of course, other implementations are also possible

2) Domain services

/**
 * Created on 2020/9/7 11:40 上午
 *
 * @author barry
 * Description: 交易服务
 */
public interface TradeService {

    /**
     * 充值
     *
     * @param tradeRecord
     * @return
     */
    TradeRecord recharge(TradeRecord tradeRecord);

    /**
     * 消费
     *
     * @param tradeRecord
     * @return
     */
    TradeRecord consume(TradeRecord tradeRecord);
}



First define the service interface. The definition of the interface needs to follow the operation of the real business . Do not use program logic or database logic to design and define additions, deletions, and changes.

  • The main thinking direction is what services the transaction object can provide externally. The definition of this service is coarse-grained and highly cohesive . Do not define some specific code implementation level methods
  • The input and output parameters of the interface should be considered in the form of objects as much as possible, fully compatible with various scene changes
  • The complex query method required by the front-end is not defined here. Generally, the query is not a domain service and there is no data change, and can be processed separately
  • The realization of the domain service mainly focuses on the logical realization, and must not include technical basic code, such as cache realization, database realization, remote call, etc.

3) Infrastructure interface

public interface TradeRepository {
    /**
     * 保存
     * @param tradeRecord
     * @return
     */
    TradeRecord save(TradeRecord tradeRecord);

    /**
     * 查询订单
     * @param tradeNumber
     * @return
     */
    TradeRecord findByTradeNumber(String tradeNumber);

    /**
     * 发送MQ事件消息
     * @param tradeEvent
     */
    void sendMQEvent(TradeEvent tradeEvent);

    /**
     * 获取所有
     * @return
     */
    List<TradeRecord> findAll();
}



  • The main purpose of putting the infrastructure interface on the domain layer is to reduce the dependency of the domain layer on the infrastructure layer
  • The design of the interface is not to expose the technical details of the implementation, such as the assembled SQL cannot be used as a parameter

4. Application layer implementation (application)

// 交易服务
@Component
public class TradeManager {

    private final TradeService tradeService;
    public TradeManager(TradeService tradeService) {
        this.tradeService = tradeService;
    }


    // 充值
    @Transactional(rollbackFor = Exception.class)
    public TradeRecord recharge(TradeRecord tradeRecord) {
        return tradeService.recharge(tradeRecord);
    }


     // 消费
    @Transactional(rollbackFor = Exception.class)
    public TradeRecord consume(TradeRecord tradeRecord) {
        return tradeService.consume(tradeRecord);
    }
}

// 交易事件订阅
@Component
public class TradeEventProcessor {

    @Autowired
    private TradeRepository tradeRepository;

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, condition = "# tradeEvent.tradeStatus.name() == 'SUCCEED'")
    public void TradeSucceed(TradeEvent tradeEvent) {
        tradeRepository.sendMQEvent(tradeEvent);
    }
}

// 交易消息订阅
@Component
public class TradeMQReceiver {

    @RabbitListener(queues = "ddd-trade-succeed")
    public void receiveTradeMessage(TradeEvent tradeEvent){
        System.err.println("========MQ Receiver============");
        System.err.println(tradeEvent);
    }
}



Application Service :

  • The application layer is a very thin layer, mainly used to call and combine domain services, and must not contain any business logic
  • Can include a small amount of process parameter judgment
  • Since it may be a combined operation call of multiple domain services, if there is atomicity requirement, you can add **@Transactional** transaction control

Event subscription :

  • Event subscription is a way to realize the cooperation and decoupling of operations in multiple fields in the process, and it is also the access point for all subsequent operations in the process
  • It is different from the combined operation of application services. The combination can be increased or decreased according to the needs of the scene, but the operation after the event subscription is relatively solidified, mainly to meet the logical consistency requirements

TransactionPhase.AFTER_COMMIT configuration is called after the previous operation transaction is completed, thereby reducing the impact of subsequent operations on the previous operation

  • Event subscriptions may have multiple message bodies. For the convenience of management, it is best to handle them in one class.
  • MQ message publishing is generally placed in event subscription

News subscription :

  • Message subscription is a one-step implementation of collaboration and decoupling between multiple microservices
  • The message body is transmitted in uniform object packaging as much as possible to reduce the processing difficulty caused by object heterogeneity

5. Infrastructure layer (infrastructure)

@Repository
public class TradeRepositoryImpl implements TradeRepository {

    private final JpaTradeRepository jpaTradeRepository;
    private final RabbitMQSender rabbitMQSender;
    private final Redis redis;

    public TradeRepositoryImpl(JpaTradeRepository jpaTradeRepository, RabbitMQSender rabbitMQSender, Redis redis) {
        this.jpaTradeRepository = jpaTradeRepository;
        this.rabbitMQSender = rabbitMQSender;
        this.redis = redis;
    }

    @Override
    public TradeRecord save(TradeRecord tradeRecord) {
        return jpaTradeRepository.save(tradeRecord);
    }

    /**
     * 查询订单
     */
    @Override
    public TradeRecord findByTradeNumber(String tradeNumber) {
        TradeRecord tradeRecord = redis.getTrade(tradeNumber);
        if (tradeRecord == null){
            tradeRecord = jpaTradeRepository.findFirstByTradeNumber(tradeNumber);
            // 缓存
            redis.cacheTrade(tradeRecord);
        }

        return tradeRecord;
    }

    /**
     * 发送事件消息
     * @param tradeEvent
     */
    @Override
    public void sendMQEvent(TradeEvent tradeEvent) {
        // 发送消息
        rabbitMQSender.sendMQTradeEvent(tradeEvent);
    }

    /**
     * 获取所有
     */
    @Override
    public List<TradeRecord> findAll() {
        return jpaTradeRepository.findAll();
    }
}


  • The infrastructure layer is the output direction of data, which mainly includes the technical implementation of database, cache, message queue, remote access, etc.
  • The basic design layer hides technical implementation details from the outside and provides coarse-grained data output services
  • Database operation: The domain layer transmits data objects, which can be split and implemented according to the implementation of the data table

6. User interface layer (controller)

@RequestMapping("/trade")
@RestController
public class TradeController {

    @Autowired
    private TradeManager tradeManager;

    @Autowired
    private TradeRepository tradeRepository;

    @PostMapping(path = "/recharge")
    public TradeDTO recharge(@RequestBody TradeDTO tradeDTO) {
        return TradeDTO.toDto(tradeManager.recharge(tradeDTO.toEntity()));
    }

    @PostMapping(path = "/consume")
    public TradeDTO consume(@RequestBody TradeDTO tradeDTO) {
        return TradeDTO.toDto(tradeManager.consume(tradeDTO.toEntity()));
    }

    @GetMapping(path = "/{tradeNumber}")
    public TradeDTO findByTradeNumber(@PathVariable("tradeNumber") String tradeNumber){
        return TradeDTO.toDto(tradeRepository.findByTradeNumber(tradeNumber));
    }

}



  • The user interface layer provides service support to the terminal
  • According to different scenarios, a separate module can provide http restful for the Web and RPG support for API calls between services
  • Provide identity authentication and authority verification services for the Web, VO data conversion
  • Provide current limiting and fuse services for the API side, DTO data conversion
  • The data conversion from the application layer to the user interface layer is more convenient for changes in requirements before different scenarios, while also ensuring the uniformity of the application layer data format

7. Complex data query

It can be seen from the above that there is no complicated data query problem. This problem does not involve business logic processing, so it should not be handled at the domain layer.

If there are more complex data query requirements, the CQRS mode can be used, and the query will be processed by a single module. If less data query can be done at the infrastructure layer, data encapsulation at the application layer, and data call at the user interface layer

  • JPA is not suitable for multi-table related database query operations, other flexible ORM architectures can be used

In the case of big data and large concurrency, multi-table association will seriously affect database performance, you can consider wide table query

3. Summary

DDD layering mainly solves the problem of coupling between each layer, so that each layer does not affect each other. In each layer, the design of the domain layer is the center of the entire system and best reflects the core idea of ​​domain-driven design. Its good design is to ensure the sustainability and maintainability of the future architecture.

Original link: https://my.oschina.net/barryhome/blog/4913300

If you think this article is helpful to you, you can follow my official account and reply to the keyword [Interview] to get a compilation of Java core knowledge points and an interview gift package! There are more technical dry goods articles and related materials to share, let everyone learn and progress together!

Guess you like

Origin blog.csdn.net/weixin_48182198/article/details/112863266