18 Development Based on Event Sourcing Design

18 Development based on Event Sourcing Design #

In the previous lecture, we demonstrated the design and development approach based on DDD through code examples. This included how to implement aggregates, design repositories, map domain objects to databases, and the technology architecture I designed based on DDD and microservices. Through these explanations, we provided a comprehensive view of how to implement domain-driven system development. However, these designs are still missing an important aspect, which is the design and development based on domain events.

Design Approach Based on Event Sourcing #

In Lecture 07 on “How to Conduct Event Storming in Online Ordering Scenarios,” we discussed the practical method of “Event Storming” in DDD. This method believes that events are facts (Event as Fact), meaning that events that have already occurred in the business domain are facts. Past events have become facts and cannot be changed, so an information management system can store these facts in the form of information in a database, which means that information is a set of facts.

Therefore, the purpose of an information management system is to store these facts, manage and track them, and thereby improve work efficiency. Based on this idea, analyzing the business requirements of an information management system means accurately identifying the key facts that need to be stored during the business process, and analyzing and designing around these facts. This is the essence of “Event Storming”.

However, after completing the analysis and design of a system using the “Event Storming” approach, how should it be implemented in system development? In the previous Lecture 08 on “How DDD Addresses the Challenges of Microservice Decomposition”, we explained how this was done by using the online ordering system as an example and implementing domain event publishing and notification mechanisms:

  • After “User Places Order” microservice completes an order, it notifies the “Restaurant Accepts Order” microservice through an event notification mechanism.
  • After the “Restaurant Accepts Order” microservice is ready, it notifies the “Delivery Rider Dispatch” microservice through an event notification mechanism.

This message notification mechanism for domain events is the design approach of “Event Sourcing”.

“Event Sourcing” is a new design approach that effectively decouples business processes that were previously coupled together, allowing increasingly complex business systems to be loosely coupled and divided into independent components. This enables component-based design and development and plug-and-play business changes. Now let’s see the differences between the design of “Event Sourcing” and traditional design through a case study.

Take the business scenario of “User Places Order” for example. From a business requirement perspective, there is a lot of uncertainty about what needs to be done after a user places an order.

For example, the initial requirement after a user places an order may be inventory deduction. The traditional approach would be to directly call the inventory deduction method after saving the order and complete the corresponding operation. Then, a new requirement of initiating logistics arises, which requires calling a method to initiate logistics and delivery. However, the story doesn’t end here. After some time, the product manager brings up the requirement of membership management, which is used to calculate member points or provide member benefits.

With each new requirement, the code for “User Places Order” needs to be modified to add certain operations after placing an order. This design makes the functionality of “User Places Order” very unstable and constantly in need of modification.

In contrast to the traditional design approach, the design approach of “Event Sourcing” is that after an order is placed, only a domain event for “User Places Order” needs to be implemented, and it is unrelated to what needs to be done after placing an order. Therefore, through the design of “Event Sourcing”, the upstream and downstream of the business process are decoupled. The upstream only needs to publish domain events, and the downstream can define its own subsequent actions based on these events, thus achieving loose coupling and maintainability of complex systems.

Design and Implementation of Domain Events #

With a clear understanding of the design approach of “Event Sourcing”, how can it be implemented? Our approach is to publish a domain event after completing the corresponding work based on the analyzed and identified domain events in the “Event Storming” process. The published content includes: event name, publisher, publishing time, and related data. For example, after a user places an order, the following domain event is published:

{
  "event_id": "createOrder",
  "publisher": "service_order",
  "publish_time": "2021-01-07 18:38:00.000",
  "data": {
    "id": "300001",
    "customer_id": "200005",
    ...
  }
}

Here, the parameters for different domain events are different. Some may be a domain object, some may be an array parameter, a map, or there may be no parameters. For example, some domain events may only involve a change in state, so they do not include parameters. Which parameters belong to which domain events is designed by the event publisher, and then the protocol is communicated to all subscribers. In this way, all subscribers can define their own subsequent operations based on this protocol.

To implement this approach in a project, the event publisher should complete the publication of an event at the end of a method. As for what events to do, it is defined by the underlying technical middleware. For example, publishing an event is done in the “User Places Order” method:

@Override
public void createOrder(Order order) {
  ...
  createOrderEvent.publish(serviceName, order);
}

@Override
public void modifyOrder(Order order) {
  ...
  modifyOrderEvent.publish(serviceName, order);
}

Note: This translation assumes that the Markdown formatting should not be preserved. Next, event subscribers need to write corresponding domain event classes for each event and define what operations the event needs to perform in the apply() method. For example, in the “Restaurant receives order” scenario, the “User places order” event is defined as follows:

public class CreateOrderEvent implements DomainEvent<Order> {

 @Override
 public void apply(Order order) {

  ...

 }

}

Event sourcing is about separating the publishing and handling of events, with the responsibility of publishing events lying with the upstream business logic and the responsibility of handling events lying with the downstream subscribers. There is only one publisher in the upstream, but there can be many publishers in the downstream, each performing different operations.

Additionally, a question worth discussing is whether every event defined in the event storm needs to be published as a domain event. For example, in an online food ordering system, the “User places order” event needs to be published, and then the “Restaurant receives order” event needs to subscribe to this event. But does the “Restaurant receives order” domain event need to be published? It seems that there is no subscriber for it downstream. However, who knows how the requirements will change in the future? When the system adds “Order tracking”, it needs to track every domain event. Therefore, we say that because we cannot predict future changes, the best approach is to publish every domain event honestly.

Publishing Domain Events Using Message-Based Approach #

We have discussed the design principles of domain event sourcing, but in order to implement it in a project, we still need support from the technical platform. For example, the business system’s publisher only handles event publishing, and the subscribers only handle subsequent operations related to events. But how should we publish events? What should we do when publishing events? And how do we implement event subscriptions? These need to be designed in the technical platform.

Firstly, the publishing party of an event needs to record the event in a database. The database can be designed as follows:

Drawing 0.png

Then, the domain event also needs to be published through a message queue, and we can use the design scheme of Spring Cloud Stream. Spring Cloud Stream is a message-driven technology framework in the Spring Cloud technology framework. Its underlying layer can support mainstream message queues such as RabbitMQ and Kafka, and provide a unified design and coding through its encapsulation.

For example, taking RabbitMQ as an example, you need to add the dependency to the project’s POM.xml:

  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
    </dependency>
  </dependencies>

Then, in the bootstrap.yml file, bind the domain event to the message queue. For example, in the “User places order” microservice, define the publishing of the domain event as shown in the following code:

spring:
  rabbitmq:
    host: xxx.xxx.xxx.xxx
    port: 5672
    username: guest
    password: guest
  cloud:
    stream:
      bindings:
createOrder:
  destination: createOrder
modifyOrder:
  destination: modifyOrder

Then, define the domain events and their clients as shown in the following code:

public interface CreateOrderEventClient {

    String OUTPUT = "createOrder";

    @Output(CreateOrderEventClient.OUTPUT)
    MessageChannel output();

}

@EnableBinding(value=CreateOrderEventClient.class)
@Component
public class CreateOrderEvent {

    @Autowired
    private CreateOrderEventClient client;

    public void publish(String publisher, Object data) {

        String eventId = "createOrder";

        Date publishTime = DateUtils.getNow();

        DomainEventObject event = new DomainEventObject(eventId, publisher, publishTime, data);
        event.save();

        client.output().send(MessageBuilder.withPayload(event).build());

    }

}

In the “create order” microservice, define each domain event in order, such as creating an order, modifying an order, canceling an order, and so on. In this way, when the “create order” microservice completes the corresponding operation, the domain event will be published to the message queue.

Finally, the subscriber completes the subscription to the message queue and performs the corresponding operation. In the bootstrap.yml file, bind the domain events, as shown in the following code:

spring:
  profiles: dev
  rabbitmq:
    host: 118.190.201.78
    port: 31672
    username: guest
    password: guest
  cloud:
    stream:
      bindings:
        createOrder:
          destination: createOrder
          group: ${spring.application.name}
        modifyOrder:
          destination: modifyOrder
          group: ${spring.application.name}

Here, a group is added. When this service is deployed on multiple nodes, only one microservice will receive and process each event. Then, define the domain event class, which listens to the message queue and specifies what actions to perform next:

public interface CreateOrderEventClient {

    String INPUT = "createOrder";

    @Input(CreateOrderEventClient.INPUT)
    SubscribableChannel input();

}

@Component
@EnableBinding(value= {CreateOrderEventClient.class})
public class CreateOrderEvent {

    @StreamListener(CreateOrderEventClient.INPUT)
    public void apply(DomainEventObject obj) {

        ...
    }

}

Now, both the “restaurant order acceptance” and the “order tracking” microservices have the CreateOrderEvent domain event. However, their apply() methods perform different tasks, allowing them to independently perform their respective jobs. For example, the “restaurant order acceptance” service sends a message to the front-end to notify the restaurant of completing the order acceptance operation, while the “order tracking” service updates the corresponding order status after receiving the message. However, regardless of who it is, each microservice will record the received domain event in its own database.

Summary #

Event sourcing is another heavyweight toolset in the DDD design practice. It decouples the upstream and downstream of domain events, separating the event publishing from what actions to perform. The upstream of the event is responsible for executing the publish() method to publish events, while the downstream is responsible for defining their respective apply() methods to perform subsequent actions. This design allows complex business processes to be decomposed into multiple components that can independently complete their tasks in a loosely coupled manner. It can also be more widely applied in the design of microservices.

By using Spring Cloud Stream’s message-driven approach to publish domain events to a message queue, we can better practice the design methodology of “event sourcing” in software projects. However, such a design requires the underlying support of the DDD technical platform.

In the next session, we will take a look at how domain-driven design is implemented in a larger-scale artificial intelligence system from a practical perspective.