Event Sourcing Part II: Implementing an end-to-end solution
*Click here for Part I
In the first part of this SSENSE-TECH series, I presented a definition of Event Sourcing, its core components and the lifecycle of the common operations executed by an application that uses it. In this second part, I will walk you through a sample implementation to provide you with a starting point, this will serve as a backdrop for further discussions on practical aspects of any application that uses Event Sourcing.
Our Domain
Imagine you are an online retail company and you have organized the operations and systems according to what is seen on Figure 1.
In this distribution, we see that when the customer decides to place an order, the shopping cart interacts with the Order subdomain. At this moment both Payment and Shipping subdomains get involved. In its simplest form, once the financial aspect of the payment has been confirmed with the Card Issuer, the Order can continue and allow the items to be shipped to the customer.
Our team is responsible for handling this fictitious Payment subdomain and we decided to use Event Sourcing. As seen in Part I, we have to highlight the evolution of the state of our system through time.
In Figure 2, we see that our payment is first created in a pending state and it can evolve over time as we interact with it. This diagram illustrates the events that will take place and make explicit the interactions (commands) we will have to serve.
Figure 3 has one command/entity/event highlighted. After conducting the analysis, we agreed upon the following list:
Entity: Payment
Each transition should contain the business invariants to control whether or not it is allowed. For example, only Pending or Authorized payments can be cancelled, or only Settled payments can be refunded.
Creating an Entity
In our domain we have an entity called Payment that encapsulates the properties and behavior that represents its possible interactions. We can see its class diagram on Figure 4, and highlight some aspects:
- The terminology used matches the ubiquitous language of the domain;
- It contains a value object (PaymentId);
- It contains a list of the events (the collection of DomainEvents);
- It has a method that indicates an event happened (record_that);
- It has a method that clears the list of events (clear_events). This will be discussed later when we retrieve the entity.
If we recall the analysis, we have a series of interactions and the related events that should be generated from a successful execution. Figure 5 illustrates one of them, the Payment Created event that should be generated when the create interaction is successful.
When designing your events, you will notice that they all have something in common: A unique identifier (aggregate_id) associated with the entity it belongs to, and when it occurred (occurred_at). Each event only contains the necessary information that represents what has changed, in this case amount_due.
So let’s see them in action. First, we begin by creating the domain event.
It is recommended that your events are immutable, and that they only contain primitive types. This will help with the process of serialization/deserialization that happens when you persist them in the event store. Now that the event is defined, let’s create our entity.
Notice that creation happens in the following manner:
- The public behavior is executed (create)
- An instance is created with just the identifier
- You notify your newly created entity that it should record that an event happened
- Any additional properties of the entity are updated
- The event is added to the list of events
Handling Business Invariants
So far, we have been able to create a Payment entity. But in real scenarios, most behaviors have business invariants associated with them. For example, when creating a payment the amount due should be greater than 0. In some cases this should be handled by a value object, but for sake of brevity, let’s do it in the entity itself.
You should perform all business validation before executing the record_that method.
Now, let’s handle another interaction. This time we will implement a refund, and for this one the rules are: the amount refunded should be greater than zero but no greater than the amount due, and the payment must be in a settled state.
PaymentRefunded needs to be defined as well. Note on the following snippet that again, we just added the information about what changed.
You can follow a similar path for the other interactions with your entity. It is important to mention that the record_that implementation presented so far is very naive, and in practice, most languages allow for more dynamic ways to bind a specific method to be called based on the event type.
In Python, one way to achieve this can be seen in the following snippet.
Persisting an Entity
Persisting an entity requires you to save your list of new events to the event store of the entity being manipulated. So far, we have been able to provide an implementation that requires no external dependencies, including the use of any framework. This was not done by chance, but to try to make your domain as environment-agnostic as possible.
In the case of the event store, while we could continue to do so, I chose not to for the following reasons:
- An event store is a low-level concern: I believe that we should focus, as much as possible, on the core of the application;
- An efficient event store can be complex to design/develop: stream life-cycle management, creation of volatile or persistent subscriptions, and combining streams are features you will likely want or need for production systems. As much as possible we should not try to reinvent the wheel.
For those reasons I selected an off-the-shelf event store called EventStore. While the code in this article by no means makes exhaustive use of its features, I found it to offer a good set of functionalities out-of-the-box, and really easy to integrate.
At its core, an event store needs to manage the creation of multiple event streams, appending items to those streams and retrieving them in an efficient manner. You can have many choices to implement yours, from using regular RDBMS, such as MySQL or PostgreSQL to NoSQL solutions using MongoDB or DynamoDB. Although this is a heated debate, there are those who even advocate the use of distributed log systems, such as Kafka for this purpose. As with any situation, evaluate your requirements, and consider the level of expertise in your teams in terms of your options, before choosing a solution.
Streams in EventStore
A stream in EventStore is identified by a unique name which, in our case, will be made of the concatenation of the entity name in plural form and its unique identifier. So, for example, if we are persisting an entity called Payment with id 1, the stream name would be payments-1.
Inside any stream, you will have the items corresponding to the events you recorded. In EventStore, this means:
- The event order: a monotonically increasing number, starting from 0;
- An internal name: generated by EventStore by concatenating the event order and the stream name;
- The event type: a string you provided when saving the event;
- The date: the date the event store received the event;
- A JSON payload: containing the actual data from the domain event, provided by you when saving the event.
A PaymentCreated event would look like the one seen in Figure 6.
In our case, I used the Repository Pattern to receive the Payment and an EventStore client to save the events.
In Figure 7 you can see there is an interface with the only two methods we will need, and a concrete implementation specific for EventStore.
Now that we have saved the Payment entity we will focus on how to retrieve it and reconstruct the state.
Finding an Entity
Our Payment entity is supposed to evolve over time. So besides persisting it upon creation, the second use case you will have to satisfy is the ability to retrieve an existing one and perform subsequent changes to it. If you refer to Figure 2, our Payment is created in a pending state and under the right conditions, can transition to other states, such as Refunded. Let’s assume that we have one Payment that was already persisted in our event store that we want to refund.
Remember that to reconstruct the state of an entity, you start by retrieving the events and applying them in the same order they were added to the stream.
Besides the syntax aspect, which can vary from language to language, the flow is:
- Recreate the entity with its id only;
- Receive the events from the stream from the beginning;
- For each one apply to the entity.
Because we are applying the events that were previously saved, there is no need to validate any business rules.
About the Source Code
You can find the full implementation of the application here. It contains all the entities, events, and repositories for the application. Please note that while the purpose is to provide a concrete implementation, it is not considered production ready as it does not include, for sake of brevity, key functionalities such as: performance optimizations, complete error handling, full use of DDD concepts, and test coverage.
So, what’s next?
In this article, l have related the definition and the relationship between the main moving parts of Event Sourcing to a concrete implementation. This not only allows you to start your own application using the language of your choosing, but also opens a deeper discussion on more advanced scenarios you should be aware of, such as creating projections, how to address very long streams of events, event migration, and GDPR. Part III of this Event Sourcing series will cover these topics, so stay tuned!
Editorial reviews by Deanna Chow, Liela Touré, & Prateek Sanyal.
Want to work with us? Click here to see all open positions at SSENSE!