(Diesen Blog-Post auf Deutsch lesen)
Before I’ll argue why I don’t share this view, I want to ensure a sufficient understanding of domain events and event sourcing:
In domain-driven design, domain events are described as something that happens in the domain and is important to domain experts. Such events typically occur regardless of whether or to what extent the domain is implemented in a software system. They are also independent of technologies. Accordingly, domain events have a high-value semantics, which is expressed in the language spoken by domain experts. Examples can be:
- A user has registered
- An order has been received
- The payment deadline has expired
Domain events are relevant both within a bounded context and across bounded contexts for implementing processes within the domain. Domain events are also ideally suited to inform other bounded contexts about specific business-related events that have occurred in the own bounded context and thus to integrate several bounded contexts in an event-driven way.
Martin Fowler describes the key feature of event sourcing in his original blog post as follows:
Event Sourcing ensures that all changes to application state are stored as a sequence of events.
Instead of storing the current state within the application directly field by field in a table in the database, reloading it when needed, and overwriting it by subsequent changes, a chronologically ordered list of events is persisted, which can then be used to reconstruct the current state in memory, if necessary.
Event sourcing is a general concept, but is often discussed in the context of domain-driven design in connection with aggregates. Therefore I use the persistence of aggregates as an example for the use of event sourcing.
The following sequence shows the relevant steps when using event sourcing for persisting and restoring the state of an aggregate:
The following advantages are typically listed for the use of event sourcing:
- The stored events not only describe the current state, but also how this state has been reached.
- It is possible at any time to reconstruct any state from the past by replaying the events only up to a certain point in time.
- It is conceivable to use event sourcing to handle incorrect processing of previous events or the arrival of a delayed event.
Having said this, the implementation of event sourcing also entails a certain conceptual and technical complexity. Events are not supposed to change once persisted, whereas the domain logic often evolves over time. The code must therefore be able to process even very old events. Snapshots are necessary in order to be able to reconstruct state based on large histories of events in a performing manner.
Also the implementation of requirements e.g. from the General Data Protection Regulation the EU (GDPR) poses real challenges for event sourcing, since event sourcing requires that no persisted events gets deleted.
Events from Event Sourcing ≠ Domain Events
So why do I think that these two concepts do not really fit together so naturally?
Let’s consider the following example: in a domain for bike sharing, a user wants to register in order to rent a bike. Of course one also has to pay for it, which is done through a pre-paid approach using a wallet.
The relevant section of the context map for this domain might look like this:
The registration process works as follows:
- The user enters his/her mobile number through the mobile app.
- The user receives an SMS code to confirm the phone number.
- The user enters the confirmation code.
- The user enters the additional details such as full name or address and completes the registration.
This process is implemented in the
UserRegistration aggregate in the bounded context
Registration. The user interacts with “his/her” instance of the
UserRegistration aggregate several times over the course of the registration process. The state of the
UserRegistration is built up step by step until the registration is completed successfully. Once done, the user should be able to charge the wallet and rent a bike.
Now, if event sourcing is used to manage the state of the
UserRegistration aggregate, the following events (containing the corresponding relevant state) are created and persisted over time:
MobileNumberValidated(no additional state)
These events are sufficient to reconstruct the current state of the
UserRegistration aggregate at any time. Additional events are not necessary, in particular no event which would express that the registration is now completed. This fact is know to the
UserRegistration aggregate due to its internal domain logic as soon as the
UserDetailsProvided event has been processed. Accordingly, an instance of a
UserRegistration can answer at any time whether the registration has already been completed or not.
In addition, each event only contains the state that is necessary to be able to reconstruct the state of the aggregate during replay. This is typically only that state that has been influenced by the invocation that triggered the event, that is, a kind of “diff”. From the point of view of event sourcing, it makes no sense to store additional state on an event that was not influenced by the invocation. So, even if an explicit event
UserRegistrationCompleted were persisted, it would not contained any additional state.
Some proponents of event sourcing vote that exactly these events from event sourcing for the
UserRegistration aggregate can also be published to other interested parties within or outside the bounded context and can thus trigger further domain logic or update some other state. In our example, these would be the two bounded contexts
Accounting (for initializing the wallet) and
Rental (for creating the registered user).
If this is to be done using the events from event sourcing, each consuming bounded context must
process these fine-granular events and know at least parts of the domain logic from the
UserRegistrationaggregate (e.g. after which event a user is considered fully registered).
combine several events to get the whole state required about the user (e.g. the phone number from
MobileNumberProvidedand additional details from
ignore events which are not of any interest in the respective bounded context (e.g.
MobileNumberValidatedto confirm the phone number)
From my point of view, this approach breaks the intended encapsulation between different parts of the system, leads to chatty communication between bounded contexts and thus increases the coupling between bounded contexts. The main reason is that the semantics of the fine-grained events from event sourcing is too low-level, both in terms of the event itself and the associated information (the “payload”).
In my opinion, things are much improved if the
UserRegistration aggregate would also publish a domain event
UserRegistrationCompleted with all relevant information
Address (but e.g. not
VerificationCode) as a payload after the registration has successfully been completed. This domain event has the appropriate semantics to be easily processed by external bounded contexts without having to know any internals of the registration process.
It is certainly possible in some cases for the semantics of an event from event sourcing to already offer appropriate semantics so that it can get processed by an external consumer in an easy way (e.g. the
MobileNumberProvided event for a hypothetical consumer who wants to know about all phone numbers registered). But even then, I opt for separating the implementation of the events for event sourcing and the domain events so that they can evolve independently one from each other. This means that there would be two representations of the domain event phone number entered in the system, each with a different purpose.
Event Sourcing and CQRS
So should events from event sourcing only be used within the corresponding aggregates?
From my point of view, basically yes. A possible and meaningful exception, however, can be the use of these events in connection with read models in CQRS. Of course, this also has impacts encapsulation, but my experience shows that read models from CQRS are often anyway rather closely linked to an aggregate, since they provide a specific view on the data of that aggregate. Thus one may argue that the coupling resulting from the processing of the fine-granular events in the read model is acceptable.
I see event sourcing as an implementation strategy for the persistence of state, e.g. of aggregates. This strategy should not be exposed beyond the boundaries of aggregates. The events from event sourcing should therefore only be used internally in the corresponding aggregate or in the context of CQRS to build related read models.
Domain events, on the other hand, represent a specific fact or happening that is relevant regardless of the type of persistence strategy for aggregates, for example, for integrating bounded contexts.
Event sourcing and domain events can of course be used both at the same time, but should not influence each other. The two concepts are used for different purposes and should therefore not be mixed.