Decrypting DDD: Domain events–the ultimate weapon for system decoupling

This is a community that may be useful to you

One-to-one communication/interview brochure/resume optimization/job search questions, welcome to join the “Yudao Rapid Development Platform” Knowledge Planet. The following is some information provided by Planet:

  • “Project Practice (Video)”: Learn from books, “practice” from past events

  • “Internet High Frequency Interview Questions”: Studying with your resume, spring blossoms

  • “Architecture x System Design”: Overcoming difficulties and mastering high-frequency interview scenario questions

  • “Advancing Java Learning Guide”: systematic learning, the mainstream technology stack of the Internet

  • “Must-read Java Source Code Column”: Know what it is and why it is so

e029971e8e99ede94d31c32da21d862a.gif

This is an open source project that may be useful to you

Domestic Star is a 100,000+ open source project. The front-end includes management backend + WeChat applet, and the back-end supports monomer and microservice architecture.

Functions cover RBAC permissions, SaaS multi-tenancy, data permissions, mall, payment, workflow, large-screen reports, WeChat public account, etc.:

  • Boot address: https://gitee.com/zhijiantianya/ruoyi-vue-pro

  • Cloud address: https://gitee.com/zhijiantianya/yudao-cloud

  • Video tutorial: https://doc.iocoder.cn

Source: geekhalo

  • 1. Application scenarios

  • 2. Domain events

    • 2.1. Internal domain events

    • 2.2. External domain events

  • 3. Spring Event Mechanism

    • 3.1. Interface-based event processing

    • 3.2. Annotation-based event processing

    • 3.3. Based on asynchronous event processing

  • 4. Spring Event application scenario analysis

    • 4.1. @EventListener

    • 4.2. @TransactionEventListener

    • 4.3. @EventListener + @Async

    • 4.4. @TransactionEventListener + @Async

  • 5. Summary

1. Application scenarios

Suppose you are a developer of order services and are developing the business function of successful payment. After deep learning DDD, you write a set of beautiful code.

@Transactional
public void paySuccess(Long orderId){
  
    // 1. Obtain and verify the validity of the order aggregate root
    Order order = this.orderRepository.getById(orderId);
    if (order == null){
        throw new IllegalArgumentException("Order does not exist");
    }
  
    // 2. Modify price
    order.paySuccess();
  
    // 3. Save the Order aggregate
    this.orderRepository.save(order);
  
    // 4. Notify the logistics service to deliver the goods
}

After successfully going online, the system runs stably. Later, you will receive more requests one after another, such as:

  1. Contact notification: After the payment is successful, a contact text message needs to be sent to the user to inform the user that the payment has been completed;

  2. Clear the shopping cart: After the user successfully purchases the product, remove the product from the shopping cart;

  3. Confirm coupon: If the user uses a coupon when making a purchase, after successful payment, the coupon service is called to mark that the coupon has been used;

  4. Risk control management: After completing the payment, call the risk control system to submit the order data for risk assessment of the current transaction;

  5. …..

More demands are still on the way. At this time, the originally beautiful code has gradually lost control and become somewhat unrecognizable:

@Transactional
public void paySuccess(Long orderId){
  
    // 1. Obtain and verify the validity of the order aggregate root
    Order order = this.orderRepository.getById(orderId);
    if (order == null){
        throw new IllegalArgumentException("Order does not exist");
    }
  
    // 2. Modify price
    order.paySuccess();
  
    // 3. Save the Order aggregate
    this.orderRepository.save(order);
  
    // 4. Notify the logistics service to deliver the goods
  
    // 5. Generate contact text messages for users
    //Send touch SMS logic
  
    // 6. Clear shopping cart
  
    // 7. Use coupons and update coupon status
  
    // 8. Submit risk control management
  
    // other code
  
}

Some questions slowly emerged:

  1. The code is corrupting rapidly: there are more and more paySuccess codes. If you want to adjust the logic, you need to read it from beginning to end. If you are not careful, you will make mistakes;

  2. Transaction pressure increases: methods are getting longer and longer, transaction boundaries are getting larger and larger, database connection time is getting longer and longer, and system performance is declining rapidly;

  3. Dependencies are becoming more and more complex: the OrderApplicationService implementation class has many external dependencies, such as logistics, text messages, shopping carts, coupons, risk control, etc.;

You may not care about these problems in the early stage, until one day an online problem occurs:

  1. There is a problem with the three-party SMS channel, affecting order payment!

  2. The shopping cart service is shaking, and the order status is still pending payment!

  3. The big data risk control service is online, and the order payment function is unavailable for a short time!

In order to prevent other people’s services from affecting you, you are smart and quietly add try-catch to each business call, but the corruption is still continuing…

If you are aware of this problem, it is a good time to introduce domain events.

Backend management system + user applet implemented based on Spring Boot + MyBatis Plus + Vue & Element, supporting RBAC dynamic permissions, multi-tenancy, data permissions, workflow, three-party login, payment, SMS, mall and other functions

  • Project address: https://github.com/YunaiV/ruoyi-vue-pro

  • Video tutorial: https://doc.iocoder.cn/video/

2. Domain events

?

Domain events are an important part of the domain model. They are used to represent some important business events or status changes that occur in the domain. They are used to capture some changes in the domain, record the business status when the event occurs, and transmit these data. to the subscriber for subsequent business operations.

?

Domain events have the following characteristics:

  1. Immutability: Domain events represent some kind of fact that has occurred, which will not change after it occurs. It is usually modeled as a value object;

  2. Decoupled system: Domain events are the core component of event-driven and are used to decouple various parts of the system, making the system more flexible and scalable. Through the publish-subscribe model, domain events are published and subscribers can subscribe by themselves, thereby achieving the purpose of decoupling;

  3. Final consistency: achieve final consistency through domain events and improve system stability and performance;

Domain events are divided into internal domain events and external domain events. To understand the difference between the two, you need to review the “hexagonal architecture” first:

8c2a880d421f6e2d2c4d6faa9a3e6935.png

  1. The inner hexagon is a domain model that carries business logic. Internal domain events are applied to the inner hexagon and are mainly used within services or components to achieve decoupling within the same service, application or bounded context.

  2. The outer hexagon is the infrastructure, carrying technical complexity, and external domain events are applied to the outer hexagon. It is used to implement communication across services, applications or bounded contexts. It is mainly used to achieve decoupling in microservice architecture, or to spread information between different subdomains or bounded contexts.

2.1. Internal domain events

?

The main goal of internal domain events is to disseminate information between domains to achieve separation of business logic and segregation of responsibilities.

?

Internal domain events are usually propagated in memory using synchronous or asynchronous methods. For example, in Java Spring, you can use ApplicationEventPublisher and @EventListener to implement synchronous or asynchronous internal domain events that are not propagated across services or applications.

Internal domain events work in memory. You need to pay attention to the following points when designing:

  1. Use the DDD model directly without converting it to DTO: all operations are completed in memory, there is no need to consider object granularity issues, it can be used directly, and there is no performance overhead;

  2. Contains basic information about the context: usually includes the time when the event occurred, the event type, the event source and any other data related to the event;

  3. Keep event processors with a single responsibility: there is a one-to-many relationship between event publishers and event processors. The event processor itself is an excellent expansion point. Do not couple the logic to the same event processor in order to reduce the number of event processors. a processor;

  4. Error handling and retry strategy: In order to ensure the reliability and robustness of event processing, when implementing event listeners, possible error scenarios must be considered and corresponding exception handling and retry strategies designed;

  5. Synchronous or asynchronous processing: Decide whether the event should be processed synchronously or asynchronously based on business needs. Synchronous means that after publishing an event, the event handler will execute immediately and the publisher will wait for it to complete. Asynchronous means that the publisher will return immediately and event handling will take place in another thread. When considering which method to use, resource competition, locking, timeout, etc. need to be fully considered;

?

Spring Event is a powerful tool for implementing internal domain events, which will be explained in detail later.

?

2.2. External domain events

?

The main goal of external domain events is to achieve distributed business logic and decoupling between systems across services or subdomains.

?

External domain events usually use message queues (such as Rocketmq, Kafka, etc.) to achieve asynchronous cross-service propagation.

External domain events work on top of message middleware. You need to pay attention to the following points when designing:

  1. Customized DTO: External domain events are propagated based on message queues, which is very unfriendly to large domain objects with huge data. At the same time, in order to prevent the leakage of internal concepts and cannot be used directly, domain events need to be customized;

  2. Event serialization and deserialization: Design the serialization and deserialization mechanism of events for transmission and processing between different systems. Commonly used serialization formats include JSON, XML, and binary serialization, such as Avro, Protobuf, etc., and message compatibility issues need to be fully considered;

  3. Event publishing and subscription: Choose a messaging middleware that supports reliable, high-performance delivery. For example, Kafka, RocketMQ, etc.;

  4. Shared event contract: The contract includes: mq cluster, topic, tag, Message definition, Sharding Key, etc.;

  5. Error handling and retry strategy: Similar to handling internal domain events, you need to consider possible errors in external domain events and design a corresponding retry strategy. In particular, problems such as loss, duplication or delay that may occur during network transmission require the design of corresponding idempotent operations, message deduplication and sequence guarantee measures;

?

Message middleware is the key technology for the implementation of external domain events. Due to space reasons, I will not explain it too much here. There will be an article explaining it in detail later.

?

Backend management system + user applet implemented based on Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element, supporting RBAC dynamic permissions, multi-tenancy, data permissions, workflow, three-party login, payment, SMS, mall and other functions

  • Project address: https://github.com/YunaiV/yudao-cloud

  • Video tutorial: https://doc.iocoder.cn/video/

3. Spring Event Mechanism

?

Spring Event is a module in Spring Framework that helps implement event-driven implementation in applications. It is mainly used for synchronous/asynchronous communication between components and decoupling event publishers and event consumers.

?

Using Spring Event involves the following steps:

  1. Define events: Create an event class that encapsulates data related to a specific event;

  2. Create an event listener: define one or more event listeners, and handle specific types of events in the listener;

  3. Publish events: Call the ApplicationEventPublisher method to publish events externally;

In Spring, event handlers can be implemented in three ways:

  1. Interface-based event processing: handle events by implementing the ApplicationListener interface and overriding the onApplicationEvent method;

  2. Annotation-based event processing: By adding @EventListener or @TransactionEventListener annotations to the method to process events, you can specify the event type and monitoring conditions;

  3. Based on asynchronous event processing: By using the @Async annotation to process events asynchronously, the response speed of the application can be improved;

3.1. Interface-based event processing

?

Due to the strong coupling with Spring, it is rarely used now and can be skipped directly.

?

The following is a sample code for interface-based event handling:

@Component
public class MyEventListener implements ApplicationListener<MyEvent> {
    @Override
    public void onApplicationEvent(MyEvent event) {
        // Handle events
        System.out.println("Received event: " + event.getMessage());
    }
}
  
public class MyEvent {
    private String message;
  
    public MyEvent(String message) {
        this.message = message;
    }
  
    public String getMessage() {
        return message;
    }
}
  
@Component
public class MyEventPublisher {
    @Autowired
    private ApplicationEventPublisher eventPublisher;
  
    public void publishEvent(String message) {
        MyEvent event = new MyEvent(message);
        eventPublisher.publishEvent(event);
    }
}

In this example, MyEvent is a custom event class, MyEventListener is a listener that implements the ApplicationListener interface and is used to process MyEvent events, and MyEventPublisher is the class used to publish events.

When the application calls the publishEvent method of MyEventPublisher, a MyEvent event will be triggered, and the onApplicationEvent method in MyEventListener will be automatically called to handle this event.

3.2. Annotation-based event processing

?

Spring provides two annotations, @EventListener and @TransactionListener, to simplify event processing.

?

3.2.1. @EventListener

Spring’s EventListener is a more concise and flexible event mechanism than traditional event listening methods. Different from the traditional event mechanism, EventListener does not need to explicitly inherit a specific event interface. Instead, it uses annotations to identify the event types that need to be listened to, and then handles all types of events through a separate listener class.

In comparison, the main advantages of EventListener are as follows:

  1. More flexible: EventListener does not rely on any specific event interface, making event processing more flexible and can monitor and process any type of event;

  2. More concise: Compared with traditional event listening methods, using EventListener can avoid a series of cumbersome interface definitions and implementations, simplify the code structure, and improve development efficiency;

  3. More loose coupling: EventListener separates the event publisher and event handler, follows the design principle of loose coupling, and improves the maintainability and scalability of the code;

  4. More testable: Since EventListener can listen to and handle any type of event, its correct function can be verified through unit testing, thus improving the reliability of testing;

Here is a simple example:

@Component
public class MyEventListener{
  
    @EventListener
    public void onApplicationEvent(MyEvent event) {
        // Handle events
        System.out.println("Received event: " + event.getMessage());
    }
}
  
public class MyEvent {
    private String message;
  
    public MyEvent(String message) {
        this.message = message;
    }
  
    public String getMessage() {
        return message;
    }
}
  
@Component
public class MyEventPublisher {
  
    @Autowired
    private ApplicationEventPublisher eventPublisher;
  
    public void publishEvent(String message) {
        MyEvent event = new MyEvent(message);
        eventPublisher.publishEvent(event);
    }
}

Compared with interface-based event processing, EventListener is a more concise, flexible, loosely coupled, and testable event mechanism, which can effectively reduce the complexity of development and improve development efficiency.

3.2.2. @TransactionEventListener

In Spring, TransactionEventListner and EventListner are both interfaces for handling events. The difference is

  1. TransactionEventListner is triggered after the transaction is committed;

  2. The EventListner will be triggered after the event is published;

Specifically, when using Spring’s declarative transactions, certain events can be triggered after the transaction is committed. This is the application scenario of TransactionEventListner. The EventListner does not involve transactions and can be used to trigger some operations after the event is published.

Here is a simple example demonstrating how to use TransactionEventListner and EventListner:

@Component
public class MyEventListener {
  
    @EventListener
    public void handleMyEvent(MyEvent event) {
        // handle MyEvent
    }
  
    @TransactionalEventListener
    public void handleMyTransactionalEvent(MyTransactionalEvent event) {
        // Handle MyTransactionalEvent
    }
}
  
@Service
public class MyService {
  
    @Autowired
    private ApplicationEventPublisher eventPublisher;
  
    @Autowired
    private MyRepository myRepository;
  
    @Transactional
    public void doSomething() {
        // do something
        MyEntity entity = myRepository.findById(1L);
        // Publish event
        eventPublisher.publishEvent(new MyEvent(this, entity));
        //Publish transaction events
        eventPublisher.publishEvent(new MyTransactionalEvent(this, entity));
    }
}

In this example, the MyEventListener class defines two methods, handleMyEvent and handleMyTransactionalEvent, to handle the MyEvent and MyTransactionalEvent events respectively. Among them, the handleMyTransactionalEvent method is marked with the @TransactionalEventListener annotation, indicating that it will only be triggered after the transaction is committed.

The doSomething method in the MyService class uses ApplicationEventPublisher to publish events. Notice that it publishes two different types of events: MyEvent and MyTransactionalEvent. These two events will trigger corresponding methods in MyEventListener respectively.

In general, Spring’s event mechanism is very flexible and can easily extend the functionality of the application. The two interfaces TransactionEventListner and EventListner have different application scenarios and can be used according to actual needs.

3.3. Based on asynchronous event processing

?

@Async is an annotation in the Spring framework, used to mark a method as asynchronous execution. Using this annotation, Spring will automatically create a new thread for this method so that it can be executed asynchronously in the background without blocking the execution of the main thread.

?

In actual applications, using @Async can greatly improve the concurrent processing capabilities of the application, allowing the system to respond to user requests faster and improve the system throughput.

When @Async and @EventListener or @TransactionEventListener annotations are used together, an asynchronous event handler will be generated. With this combination, event handlers are executed in a separate thread pool to avoid blocking the main thread. This method is very useful when a large number of events need to be processed or the event processor takes a long time, and can effectively improve the performance and scalability of the application. At the same time, the Spring framework also provides complete support for this method, which can be easily used to implement asynchronous event processing.

Here is a simple example code that demonstrates how to use @Async and @EventListener together to implement asynchronous event processing in Spring:

@Component
public class ExampleEventListener {
  
    @Async
    @EventListener
    public void handleExampleEvent(ExampleEvent event) {
        //Execute asynchronous logic in a new thread
        // ...
    }
}

In this example, the handleExampleEvent method in the ExampleEventListener class uses @Async and @EventListener annotations, indicating that this method is an asynchronous event listener. This method is executed asynchronously when an ExampleEvent event is triggered. In this method, you can perform any asynchronous logical processing, such as sending messages to the queue, calling other services, etc.

Note: When using @Async, you need to customize the thread pool according to the business scenario to avoid insufficient resources (Spring uses a single thread to process @Async asynchronous tasks by default)

4. Spring Event application scenario analysis

To sum up, when domain events are sent out, different annotations will produce different behaviors, which are briefly summarized as follows:

30a86bcef0c800da861fd052ef7a09de.png

4.1. @EventListener

e53f101f5a7407c8981ab81d3e1268f7.png

Features:

  1. Executed sequentially. After calling publish(Event), the call to the @EventListner annotation method is automatically triggered

  2. Executed synchronously. Use the main thread to execute. If the method throws an exception, it will interrupt the calling link and trigger the return of the transaction.

Application scenarios:

  1. Transaction message table. Complete modifications to business data and message tables in the same transaction

  2. Business verification. Verify the business object for the last time. If the verification fails, an exception will be thrown directly to interrupt the database transaction.

  3. Business plug-in. Execute plug-ins in the current thread and transaction to complete business expansion

4.2. @TransactionEventListener

80189176b8189aa736036e1496a171a4.png

Features:

  1. Executed after the transaction is committed. When publish(Event) is called, a callback is only registered in the context and will not be executed immediately; the execution of the @TransactionEventListner annotation method will only be triggered after the transaction is committed.

  2. Executed synchronously. Use the main thread to execute. If the method throws an exception, the calling link will be interrupted, but the transaction will not be returned (the transaction has been submitted and there is no way to return)

Application scenarios:

  1. data synchronization. After the transaction is committed, synchronize the changes to ES or Cache

  2. Keep audit logs. Only record business changes when they are successfully updated to the database

?

Note: @TransactionEventLisnter must be in the transaction context. Without the context, the call will not take effect.

?

4.3. @EventListener + @Async

ae09647e5a4d171b717559b1dee5f33f.png

Features:

  1. Executed sequentially. After calling publish(Event), the call to the @EventListner annotation method is automatically triggered

  2. Executed asynchronously. Use an independent thread pool to perform tasks. Exceptions thrown by methods have no impact on the main process.

Application scenarios:

  1. Keep detailed logs to help troubleshoot problems

4.4. @TransactionEventListener + @Async

be4abb9b5a76e6c750b122dfb6a4ce5c.png

Features:

  1. Executed after the transaction is committed. When publishing(Event) is called, a callback is only registered with the context and will not be executed immediately; the call to the @TransactionEventListner annotation method will only be triggered after the transaction is committed.

  2. Executed asynchronously. Use an independent thread pool to perform tasks. Exceptions thrown by methods have no impact on the main process.

Application scenarios:

Asynchronous processing. Record operation logs, save data asynchronously, etc.

Note: @TransactionEventLisnter must be in the transaction context. Without the context, the call will not take effect.

5. Summary

Domain events are a powerful decoupling tool in the system, including:

  1. Internal events complete the decoupling of components within the domain model;

  2. External events complete the decoupling of domain services;

Spring Event is a powerful tool for decoupling internal domain events. Based on the combination of event listening annotations and synchronization/asynchronous annotations, it provides different support for different application scenarios.

656c07512ba3378b524104f533a354e8.png

External domain events are strongly dependent on the use of message middleware, which will be explained in detail in an article later.

Welcome to join my knowledge planet and comprehensively improve your technical capabilities.

To join, Long press” or “Scan” the QR code below:

c5fc139dc6338d7dfc6c8c44e4180b1f.png

Planet’s content includes: project practice, interviews and recruitment, source code analysis, and learning routes.

07c4f140263cd112bf9af8a7e32ff206.png

2660585eb06ee8df1f55bb8f79a113f5.pnga1815916e1bda85e7ccf54f1385e6e9a.png bd79fbe2f4a2ce45ee24a9508f5902a6.png93f48e33e4d768f 2519dd82c9bf8ec66.png

If the article is helpful, please read it and forward it.
Thank you for your support (*^__^*)