Event Sourcing: A Matter of Definition
Understanding Event Sourcing Basics
Event Sourcing is a straightforward pattern to both use and understand. However, why do we encounter numerous strong opinions, challenges, or even failures? The primary reason is usually due to confusion. In this article, I will attempt to elaborate on my thoughts by demystifying the definitions to reduce this confusion.
The Definition
Thanks to the community’s advocacy, and particularly the efforts of Oskar Dudycz, we can observe a decrease in the level of confusion between Event Sourcing and Event Streaming or Event Driven. Sometimes, it’s also easier to grasp a pattern when we initially learn when it should not be applied.
To put it concisely, Mathias Verraes’s definition is exceptionally clear:
A system is Event Sourced when:
- the single source of truth is a persisted history of the system’s events;
- and that history is taken into account for enforcing constraints on new events.
Once the Event Sourcing definition is well understood, we are already halfway there. However, to reach that point, it’s also necessary to understand what an Event Store is in practice. Event stores are essentially key-value databases with features such as atomic writes, optimistic concurrency or idempotency. The key usually consists of a name and an ID, while the value is an ordered list of events. This key-value pairing is commonly referred to as a stream aka history.
Stream’s life-cycle
Keep it short! This means short enough to read the entire stream without any performance issues. To achieve this, prior modeling is crucial. Event Modeling is an excellent method to discover and describe a system by showing how information changes within it over time. During the modeling we can define boundaries and independent streams, which shows us how to keep them short. It’s always less expensive to refine or redo a model rather than realizing it was a mistake after implementation or release.
If this is not possible, my advice is to avoid Event Sourcing altogether! It’s worthless. Big streams are typically anti-patterns and are disguised as CRUD operations, cumulative data, or Command Sourcing.
To clarify with an example, auditing is one of the outcomes of Event Sourcing, but we don’t do Event Sourcing solely for the purpose of auditing. So it shouldn’t be used for updating entity information. There are other suitable patterns for this.
The snapshots technique is also too complex and must be at the very bottom of the decision tree.
State VS Projections
The final boss! Which is the confusion between the state and projections. If we aren’t familiar with earlier definitions, failure can happen quickly. But this one can hurt slowly. It’s hidden behind many tricks and fixes. However, it’s an easy boss to fight because it’s again a matter of definition.
State
The state is private. It’s built only in-memory each time we load the history. That’s all! As said before, it’s used to enforce constraints on new events. Jérémie Chassaing’s explanation is quite straightforward:
- Reduce the history by evolving each event
state -> event -> state, to get the current state. - Decide
command -> current state -> events, to get the command’s behavior as new facts. - Append new events to the history.
Projections
On the other hand, projections are public and it’s very important to point out that there could be many of them. Every time new events are appended to the history, projections are triggered right after. Like the state, the history is replayed, but this time, it’s to determine how to update the read model.
What’s interesting with projections and therefore read models, is that what is projected is concise and simple and doesn’t contain all the internal state or unnecessary data. They are optimized for the view and it’s cheap to have many of them. No need to have a complex relational database.
Common misuses are to project the state, to project from one event and not from the history, to not optimize the read models for views, or to make a canonical or monolithic model.
Conclusion
Event Sourcing is a straightforward pattern to both use and understand. However, it’s crucial to grasp the definitions, regardless of implementation details.