Domain Event: The Missing Building Block

Domain Events

Domain-Driven Design (DDD) is a pretty broad subject. It ranges from the higher-level strategic design concepts all the way down to its building blocks. The building blocks of DDD consist of patterns such as aggregates, services, entities, value objects, repositories and factories. These patterns not only help us create richer domain models, but they also facilitate communication about software design, since these few words convey so much knowledge in such a concise way that, when they are used, engineers immediately get context regarding what is being talked about.

That’s all well and good. There used to be a fundamental problem with the building blocks, though. The problem was the original list was incomplete. When Eric Evans wrote the blue book, he missed an essential building block that only came up later: the domain event. [1]

Now, before talking about domain events, let’s first simply define what events are, without the domain part. There are two main characteristics that define events: first, they have already happened; and secondly, as a result, they cannot be changed. In other words, events are immutable facts about the past. If we were building a board game, for example, we could design objects such as GameStartedEvent and MovePlayedEvent.

Okay, now we know what events are. But what are domain events? Domain events are simply events that belong to the domain model. If you are wondering how to distinguish between domain events and other events, just ask yourself: do domain experts care about it? If the event is not relevant to the business at all, then it’s not a domain event.

The next question that comes up is: why are domain events necessary? Why are they considered an important piece that was missing from the building blocks of domain modeling? The need for domain events often arises in distributed systems, like microservices architectures. When a microservice updates its state, other services might be interested in that change. A way of solving this is for that service to publish a domain event to which interested services can subscribe. This helps decoupling services as they are only communicating through domain events.

Even though the domain event pattern helps with decoupling, we need to be careful when applying this pattern. We have to remember that the contract of a service is not only its REST API. Given that domain events are visible from outside the service, they are part of the contract of that service as well. Therefore, changes to the event affect all consumers of that event, and not just the event producer. That means usual SOA governance practices apply, such as contract versioning, testing and documentation.

Besides its potential benefits, the domain event pattern is not only valuable in and of itself, but also necessary for implementing other patterns. One of the patterns that require the use of domain events is event sourcing.

Event Sourcing

Event sourcing is a completely different programming model compared to what developers are usually used to. It requires a strong paradigm shift and a substantial shift in thinking. Instead of the usual in-place updates that developers are allowed to perform when they want to change the state of the application, when applying event sourcing, they are only allowed to add new events, and never to alter what was recorded before. That is in accordance with the definition of event. So, in this paradigm, domain events become first-class citizens. The domain events are the source of truth. Thus, in event-sourced systems, the state of the application is maintained as a sequence of immutable facts (the domain events), which are persisted in a store of data called the event store. The event store is generally implemented as an append-only log.

Despite requiring a very different mindset compared to traditional programming, the event sourcing model brings several benefits to the table. First of all, since domain events are the source of truth, it enables loosely coupled microservices architectures, in which services distribute their data through those events. Secondly, we are always able to replay the events in the same order in which they actually happened, which can help with troubleshooting and bug fixing, since we can reprocess the events after bugs are fixed. Also, another great advantage of the event store is that it enables a forward-compatible architecture. That means we can build new applications in the future that depend on domain events that have already been implemented, and we can plug those new applications in the event store so they can process all events from the very beginning of history. Besides all of that, there is also a problem that event-sourced systems, unlike other systems, don’t have to struggle with: the problem of dual writes. Because the domain events are the single source of truth, there is no dual write in event-sourced systems.

Event sourcing does bring lots of benefits as well as architectural options, but we do have to remember that there is no silver bullet. In addition to the more complex programming model and its higher learning curve, event sourcing also makes it more difficult to query the current state of the application, given that it requires transforming the domain events into something suitable to query. Let’s talk about another pattern that helps us with just that.

CQRS

In microservices architectures, data is distributed and each service (ideally) has its own database. Yet, there is still relationship between data. So how do you perform a query that joins data from multiple services? Furthermore, in an event-sourced system, the event store is not easily queryable by the application.

CQRS, which stands for Command-Query Responsibility Segregation, is a pattern that uses domain events in order to build read-only views of data, known as projections. So the solution it proposes is to duplicate data and to design the different replicas of the data to support specific queries. Different services keep their replicas up-to-date by subscribing to the domain events published by the owner of the data. Since the events produced in one bounded context may be consumed in other bounded contexts, different models are allowed to be used. [2] And that’s one of the options we get by segregating the commands responsibilities from the queries’: we can have different models for reading and writing. While the command handler is generally concerned about validating the commands and satisfying the business rules, the query handler is concerned about optimizing reads according to the access patterns of the application.

Applying the CQRS pattern provides loose coupling between reads and writes, which in turn leads to scalability and performance gains as reads and writes can be scaled independently. Not only that, but the various projections themselves can scale independently, and each of them can use the storage technology that best suits their application’s access patterns, effectively enabling polyglot persistence. A search application, for example, may store its projection in Elasticsearch, while another one may cache it in Redis.

An important aspect of this pattern to keep in mind, however, is that the subscription to domain events and the subsequent update of projections are asynchronous processes. Therefore, the CQRS projections are eventually consistent. This might or might not be an issue. It will depend on the application requirements and the needs of its users.

Code

As Linus Torvalds once said, “Talk is cheap. Show me the code.” I’ve implemented this tic-tac-toe application using event sourcing. [3] I used Kafka as the event store, saving all domain events in a Kafka topic. Here is the architecture:

Figure 1: Event-Sourced Tic-Tac-Toe’s Architecture

Conclusion

I cannot stress this enough: there is no silver bullet in software design. The domain event pattern is indeed a critical building block of DDD that was missing from the original list, and it does open up very interesting possibilities regarding domain-driven modeling and design. But its potential benefits do not free us from doing proper requirements assessment, comparing potential solutions, analyzing the trade-offs and understanding whether they pay off or not in order to make informed decisions.

References

  1. https://www.youtube.com/watch?v=lE6Hxz4yomA
  2. https://rafaelritter.medium.com/ddd-model-integrity-patterns-36d0e8ac9561
  3. https://github.com/rafaritter44/java-pocs/tree/master/event-sourcing

Software Architect at ilegra