This is the second post in my series on event sourcing. Last time, we saw a simple implementation based only on projections. While simple, it can only be queried by the event stream ID. In this post, we look at an approach that allows for more lookup criteria while still avoiding a read model.
Lookup criteria
We can directly look up an event stream and project the events when we have an event stream ID. This is often the case because we get the ID from another thing* and the event streams are short enough to just project them every time we need the data**.
* I use the term thing because I don’t like the terms aggregate or entity because they carry too much meaning not relevant in this context.
** I’ll come back to keeping event streams short later in the series.
For all other cases, we need a way to find the right event stream(s). The typical solution is to introduce a read model that enables searching for the required data. But read models are the content of the next post in this series.
In this post, we try to avoid read models and all their drawbacks with another trick.
Extending Events with search data
Instead of persisting minimally designed events to a database, we extend the “rows” (SQL-speak) with additional data.
In the expense sample in the last post, I mentioned that there are additional IDs and the workday on every expense event.


This is not a necessity. The ExpensePositionId, EmployeeId, and Workday could also be stored in the ExpenseEntered event.
The reason these fields are repeated on every event is that we can now, for example, search all events of a specific employee on a specific workday. Then we can project all these events into expenses.
Advantages
The big advantage of this approach is that we don’t need a read model, we need less DB storage space, and we don’t have to deal with consistency.
It is also worth considering when using bi-temporal event sourcing (an event has two dates: when it entered the system and when it takes effect), because implementing bi-temporal read models is really hard. More on this later in the series.
Disadvantages
Search capabilities are, of course, very limited. We can only search for fields that we store with every event.
And we need to extend every event with this data. Sometimes this means we even need to read it from storage first – e.g. read the old expense because we otherwise don’t know the workday.
A workaround FOR storing all search fields on every event
There is another similar approach that doesn’t need all the search fields on every event. We could, for example, store the employee ID and workday only on the ExpenseEntered event. When we need to load all expense events for an employee on a workday, we could write a query that is a bit more complicated:
- Find all
ExpenseEntredevents that match the search criteria - Get all
ExpenseIds from these events - Load all expense events with these
ExpenseIds
This works quite well on SQL-based storages. However, the queries are obviously a (bit) slower.
Finally, we have to make sure we filter out false positives, or be sure they can’t happen. A false positive could occur when an event changes one of the search field’s values. In that case, it is probably better to switch to a read model completely. But as experience shows, requirements may change, which can turn an event stream that matches this approach well into a bad match.
We use this approach mainly with bi-temporal event streams. Follow the series along to see why.
Conclusions
There is a middle way between simple “project always” event sourcing and the use of read models. It avoids the disadvantages of read models, but lookup options are limited. This approach is especially useful in bi-temporal event sourcing.
In the next post, we’ll look at read models.
[…] Event Sourcing: A simple trick to get around read models (Urs Enzler) […]