Domain-driven design practice — Domain Events

Chaojie Xiao
7 min readOct 9, 2022

--

Photo By maxican on unsplash

Domain-driven design (DDD) is a methodology to guide engineers on how complex business problems and system designs are modeled. Following the last blog: Domain-driven design practice — Modeling payment systems, we will continue to introduce how we practice the DDD pattern. In this blog, we will focus on how to use Domain events to make systems more scalable and resilient.

What is a Domain Event?

Eric Evans, the author of the book: Domain-driven design, defines Domain Event as

A domain event is a full-fledged part of the domain model, a representation of something that happened in the domain. Ignore irrelevant domain activity while making explicit the events that the domain experts want to track or be notified of, or which are associated with state changes in the other model objects.

More straightforwardly, we can say Domain Events are Events related to the changes in the Domain models(aggregates), which Domain Experts care about. For example, In the payment system, when a payer pays the money on the checkout page, the payment is paid. At this time, we may say that PaymentPaid is a domain event.

Why do we need Domain Events

In a microservice architecture, one user command may have changes on multiple bounded contexts. For example, in the payment system, when a payer paid the money on the checkout page, the PaymentPayCommand is executed, which may have the following effects on several bounded contexts:

  1. PaymentCore: Payment status should be changed to PAID
  2. PaymentSettlement: Payment Settlement data should be calculated
  3. PaymentFusion: Payment View Details should be updated.

In this scenario, we have to maintain eventual data consistency between multiple bounded contexts, including PaymentCore, PaymentSettlement, and PaymentFusion. Usually, there are two solutions for this problem.

  • Orchestration: There is an orchestration layer to coordinate interactions with different bounded contexts
  • Choreography: each microservice does its own tasks and publishes domain events that trigger tasks in other bounded contexts

In the example scenario above, we think Choreography is more suitable because

  1. Update on the PaymentSettlement and PaymentFusion bounded contexts are asynchronous and parallel, so there is no need for an orchestration to coordinate the request and response.
  2. More scalable and resilient: there is no central controller and no dependency between different services.

In our practice, domain events are used as a great tool to implement choreography pattern that explicitly implement side effects on aggregates or bounded contexts and decouple them. In this way, the system can be made more scalable and resilient.

Domain Events vs Event Sourcing

Before we get into how to use domain events, we want to introduce event sourcing and clarify their difference, because sometimes they are easily confused in the context of domain driven design.

Event sourcing is a way to store the state of the object. Instead of storing just the current state of the object, use an append-only store to record the full series of changes taken on that object.

Domain Events and Event Sourcing are easy to be mixed up because they both relate to “event” and sometimes they can be used to together, but they are absolutely different concepts.

  • Domain events: something happened in the domain
  • Event sourcing: a way to store the state of domain object.

How to use Domain Events

In this section, we will try to introduce how to implement the Choreography pattern by using domain events

For example, when a user sends a PaymentPayCommand, which will change the payment status to PAID, the domain event PaymentPaidEvent is sent to notify that the aggregate Payment has been paid. In the domain event handler for the PaymenPaidEvent, we can explicitly put side effects on business logic. For example, notifying the payment fusion bounded context to update the payment details and, the payment settlement bounded context to calculate the settlement amount and fee.

Here we use ApplicationEventPublisher provided by the Spring framework to implement domainEventBus. You can replace it with any event bus you like.

By introducing Domain events, we can decouple the changes in PaymentCore bounded context with PaymentSettlement and PaymentFusion bounded context. In general, domain events can give us the following benefits:

  • Integration with multiple bounded contexts parallel and asynchronously.
  • Improve performance and scalability, pushing for eventual consistency.

But there are still some problems in the above example:

  • No transaction guarantee for saving domain aggregates to databases and publishing domain events.
  • If the event handler fails due to application restart or bugs in the code, the event handler will not be retried.
  • No storage of domain events for the aggregate, so there is no change history of the aggregates.

Domain Event Framework

We build a domain event framework to solve these problems with the following functionalities.

  1. Save the aggregates and domain events into the database in one transaction. After the transaction is committed, the domain events will be published.
  2. Domain events retry and audit.

In the above diagram, we implement an Outbox Pattern to ensure aggregate Payment and domain event PaymentPaidEvent are saved into a database in one transaction before the PaymentPaidEvent is published to PaymentPaidEventHandler.

Transactional

Since domain aggregate and domain events are saved into a database in one transaction, it is necessary to add a local event table in the application service. The suggested domain event table schema is:

CREATE TABLE IF NOT EXISTS domain_event_store (
id varchar(256) NOT NULL,
event_body JSONB NOT NULL,
event_type varchar(128),
aggregate_id varchar(128) NOT NULL,
status varchar(64) NOT NULL,
version int NOT NULL,
created_at timestamp,
updated_at timestamp,
constraint domain_event_store_pkey primary key (id)
);

We need to ensure the aggregate operations and domain events persistence must be executed in one DB transaction for strong consistency. To achieve this, we provide an interface: DomainEventEnhancedDomainRepository, and the domain repository just needs to implement this interface. There is an aspect that will enhance the repository with two functionalities:

  1. Save the aggregates and domain events into the database in one transaction, with success or failure at the same time.
  2. Publish the domain events using domainEventBus after the transaction is committed into the database. Here we use @TransactionalEventListener to ensure the transaction is committed before the domain events can be processed by EventHandler.

Replay and audit

All the Domain events are saved in the table domain_event_store so that we can query all the change history on the aggregates to replay the impact on the observed aggregates or bounded contexts. Meanwhile, it can also serve as audit logs.

Sample code

A Domain event contains some common fields, e.g. created time, event Id, and a payload which is an AggregateRoot

For the Payment example mentioned above, we can create a PaymentPaidEvent like this

To ensure aggregates and domain events can be saved into a database in one transaction, we need to define a domain repository that implements DomainEventEnhancedDomainRepository so that the aspect can enhance it.

Finally, we can add domain event PaymentPaidEvent to the aggregate root Payment and execute save on the enhanced domain repository to save the data into database and publish domain event

Conclusion

In this blog, we introduced one of the key concepts in Domain-Driven design: domain event, and how we practice using it in payment systems. Domain events can help decouple services for better scalability and resilience, especially in the microservices architecture.

Meanwhile, we introduced a domain event framework, which implements an outbox pattern to ensure transactional operations for aggregate and domain events in the database, while pushing the eventual consistency between different bounded contexts.

In the future, we will continue to dive deep into other topics in the DDD pattern, like event sourcing, layer management, context map pattern, etc., and how to apply them in the system design.

--

--