Event sourcing is considered to be one of the more complex concepts in development, but it’s also one of the most intricate and exciting approaches we’ve used in a project. While we worked with event sourcing in Laravel, this general guide should be helpful to any developer since it covers all the essential details about this method, both independently and with CQRS, featuring real-life examples. So, let’s jump right in.
A Guide to Event Sourcing
In modern systems, the common practice is to build systems around databases that store the system’s current state. The primary data source is the information stored in these database tables. However, event sourcing introduces a different method.
In this approach, the current state is obtained as a result of past events. This method is usually used in fields like finance, accounting, insurance, medical, etc.
Unlike event logging, which records events without changing the current state, event sourcing generates the current state based on previous events. Think of the current state as a cache – it can be deleted and reconstituted at any time. But it’s important to understand that the current state is changeable and is obtained due to past events, so the reliable data source is the event, not the state.
Let’s consider a simple example: suppose you put some money in a bank account and make several transactions over time. After some time, you review your bank account and notice a difference in the current balance, so you contact the bank to clarify the current situation.
Imagine the banking system operates as a typical CRUD application. Detecting errors becomes impossible because every transaction leads to multiple changes in the bank account balance. The database consistently overwrites the old value with the new one, so the old value will never be available again. Therefore, it becomes impossible to identify the amount of money spent by the customer.
Event sourcing comes as a solution to this problem. It allows us to record all the transactions the user has made and, based on these transactions, reconstitute the user’s balance after eliminating the error in the code.
Understanding Events in Event Sourcing
We’ve mentioned the term “event” earlier, so let’s elaborate on it a bit. In this context, an event refers to a specific occurrence that represents the object of our interest. When discussing events, we refer to them in the past tense. For example, consider the event “Order Placed.” Events should comprehensively describe the occurrence and include only useful data. In the case of “Order Placed,” this data comprises:
- The ID of the user who placed the order
- List of ordered products, quantities and prices
- Billing and shipping addresses
- The time when the order was placed
- Information on taxes
- Information about payment methods, etc.
Event sourcing is a unique approach to storing data. Here, data records are only added and never deleted or changed. So, once an event is registered in the system, it becomes unchangeable.
You might wonder how adjustments can be made to the data provided by an event. For this, a new “correction event” is sent to the system in a modified form, which accordingly updates the current state of the system.
Changing the initial values of an event could lead to data loss. Since we never know which data might be more important or necessary in the future, we never change event data. By maintaining this unchangeable nature, event sourcing eliminates the risk of data loss- which is actually a significant advantage of this approach.
A distinctive feature of event sourcing is the ability to “time travel,” which means that you can restore the state at any point in time, which gives you the opportunity to see what caused a bug in the system.
Event Sourcing With CQRS
CQRS and event sourcing are often used in combination. But what is CQRS? This is the separation of data writing and reading. While it’s possible to use event sourcing and CQRS independently, they are often harmoniously combined and employed together in various systems.
Let’s now explore the architectural components of event sourcing, which includes four key components:
- Event store
- Reading with Queries
- Writing with Commands
Event Store and Queries
Consider the event store as a mechanism for recording events. Its role involves storing events when they occur (write) and calling all events during fetching (read) using offsets and scopes.
To display information to users, we use queries to read and “generate” information. This “generation” is necessary because users are generally interested in the current state, not just the individual events. This is particularly crucial when dealing with a series of chained events from which we need to get the final result.
Now, let’s explore the concept of projection, which can also be imagined as a table in the database. This table is where the necessary data is generated to determine the current state of the system. The projector creates projection. The system already employs two database tables – one for recording events (“event store”) and the other for reading the current state (“events projections”). This aligns with the event sourcing and CQRS concept.
User requests directed to the system essentially queries to the database, are handled by the query handler. This handler retrieves data from the event store, generates a projection based on the received data, and subsequently returns information from the projection. The same happens with repeated requests.
We also know that users need to store data. Writing data to the database is not done directly, as there is a risk of SQL injections. This is because event sourcing doesn’t involve validating data from users directly; instead, command handlers are employed for this.
Think of a command as the user’s intention to make changes in the system. Unlike events, these intentions (commands) may be rejected, as they are not yet facts. As mentioned earlier, when selecting event names, we use the past tense (like “Order Placed”). However, during commands, we use the imperative form (like “Place Order”). If a command successfully passes validation, it is recorded as a new event in our system, encompassing all the provided data.
What actually validates the command is called an aggregate. For an aggregate to appropriately validate a command, it needs to know the current state. Aggregate uses events, not projections, to reconstitute the current state. It retrieves all records related to a specific aggregate by identifier from the event store. Then, it replays all events until it reconstructs the current state. Finally, based on this current state, it evaluates the validity of the user-sent command.
You might wonder if this process, often termed “event replay,” could be slow. Indeed, it can be. So, we should try to have as few events as possible associated with the aggregate. If performance issues persist, snapshots come into play. A snapshot is the system state at a particular moment, offering a solution to potential slowdowns.
Reactors represent the final component in the list I discussed earlier. Their main function is to respond to events. This means that if a reactor is associated with (subscribes to) a specific event, it processes the data supplied by that event, applies its own business rules, and makes decisions based on it. The reactor might initiate external requests, trigger a new event (sent to the system), or even perform both actions simultaneously.
It’s important not to confuse reactors with projectors, which are responsible for creating projections. A reactor is designed to perform a specific task. For the most part, during state reconstitution (event replay), reactors don’t send external requests. There are two types of reactors: stateful and stateless. If a reactor requires a state, it manages and provides it on its own.
Final Thoughts on Event Sourcing With CQRS
To sum up, event sourcing helps prevent data loss by keeping track of every change in the system. When combining event sourcing with CQRS, it becomes more flexible, potentially improving system performance.
However, it’s important to consider that event sourcing may not be the best fit for smaller projects because of its complexity. Its use depends on your project’s challenges and needs.
Get in touch
Business development manager
Business development manager