At Culture Amp we are increasingly embracing event sourced systems for a few reasons:
- Architectural modularisation and flexibility
- The strong emphasis on Domain Driven Design and modelling, best representing the business domain
- The semantically meaningful append-only event streams mean we never have to throw away data, allowing us to leverage this highest-possible-fidelity historical data in new and interesting ways across our systems
As we hire more and more people and do more and more event sourcing, we're looking for easy ways for newcomers to gain a grasp of the basics.
I was searching for an example domain that could represent the basic concepts of event sourcing with:
- Little-to-no text, favouring shapes, symbols, boxes and arrows, in order to emphasise the mechanics of how things sit together
- A well-known, bounded domain
I realised Tetris ticks both of these boxes, with its minimalist, iconic shapes and sheer oldschool ubiquity - everyone has played Tetris!
The Tetris Playfield also has the benefit of being an entity that is subject to forces other than just the Player, it is affected by new blocks coming in from outside (in reaction to the previous block landing), and subject to the ticking of time (gravity moving the blocks down the Playfield).
Here is an example of how an event sourced implementation of Tetris might look, and below we will unpack it, piece by piece.
An aggregate instance is the central entity to which things happen. In many event sourced systems there can be many instances of many different types of aggregates. Aggregates tend to have a life-cycle: a creation event, many update events, and, often but not always, an ending event.
In the Tetris example, the
Tetris Playfield Aggregate is instantiated every time a new game is started. In this implementation, the first thing that happens (the creation event) to it is a
New Block Appeared at the top of the Playfield. Each instance of the aggregate has an id, in this case, ids
One form of input are user commands, which represent the user intention to do something to an aggregate, and which may succeed or be rejected.
In the Tetris example, the user may attempt to nudge the block left or right, rotate it clockwise or anti-clockwise, or send it immediately to the bottom of the Playfield. If the user tries to move the block through a wall, the user command will be rejected.
When an aggregate doesn't reject a command, it sinks one or more events to the event store. The aggregate can then use these events to build up "just enough" state in order to accept or reject subsequent commands.
In the Tetris example, when the movement commands are accepted by the
Tetris Playfield Aggregate based on the current state of the Playfield, the "update" events
Rotated Anti-clockwise and
Sent To Bottom are saved as events in a time ordered append-only fashion.
Apart from user commands, scheduled commands, reactors or other actors may act upon an aggregate.
It's a common pattern to have "scheduled commands" in event sourced systems.
In the Tetris example, we have
Time running on a schedule (once per second), interacting with the aggregate. It's the ticking of
Time that allows blocks to move down the playfield, and when the aggregate detects that the block has reached the ground, the aggregate can emit an additional
Landed event following the
Reactors have the additional feature that they can listen to the events that have happened so far, and react to those events by sending new commands back into aggregates.
In the Tetris example, we want the
Landed event to trigger a
New Block Appeared at the top of the Playfield.
With the combination of blocks being able to "land" and new blocks being able to enter the Playfield, this means the aggregate can now sink
Line Cleared or
Tetris Cleared events, and of course
Game Over "ending event" when the Playfield gets full.
So far we've only spoken about "write" actions, things that attempt to change the system and sink events. In event sourced systems, we usually want to be able to view or "query" the state of the system in various context-specific forms.
"Projectors" allow us to "project" the events in the event store into different views or "projections". Projectors (and Reactors) only need to listen for events with relevant types and can skip the rest, as indicated in the diagram.
In the Tetris example, the Player needs to be able to view the current state of the Playfield so that they can decide which further input commands they wish to send. Once the game is over, they can see a summary of what happened during the game, including their final score. The act of requesting data from a projection is called a "query"
With the following basic building blocks we have a full end-to-end eventsourced system:
- Projectors, projections and queries
Tetris is a great domain to illustrate the fundamentals of event sourcing, but in reality, one probably wouldn't build a video game using event sourcing because video games need to optimise for very low latency throughput. Event sourced systems optimise instead for domain richness, data fidelity, architectural modularisation and flexibility. In future articles we will dive into how to to actually build these systems with code.