Deriving state from events
Jakub Zalas
Posted on November 10, 2023
Now that we have discussed a functional approach to event sourcing and have chewed over an example implementation, let's consider the state in an event-sourced domain model.
In event sourcing, the state is derived from events that have happened in the past. In a classic approach, it's the state that's persisted. In an event-sourced system, it's the events that are persisted.
Events as state
The most direct thing we can do is to filter events on the fly for the facts that we need to make decisions.
Indeed, this is exactly what we've done in the last article. We defined a bunch of extension properties and functions that work on the event list.
Notice that, since we have no guarantees that certain events are in the stream, we often need to handle missing events by providing default values (like in Game.availablePegs
) or returning null
(like in Game.secret
).
Other than that, the usage is pretty similar to what we'd have if we had a proper class representing the state.
This is a good way forward with short streams and a relatively small number of query operations. It's also a good way to figure out what kind of properties and functions we might need for our state implementation. Once we learn what's required, we can always refactor to a stream aggregation.
Event stream aggregation
Event stream aggregation is a process that builds the state from a stream of events. It starts with an initial value and applies events one by one in order. Each application of an event returns an updated state. In other words, stream aggregation is a projection of events to a state.
What then gets passed to our domain functions is an instance of the built state rather than the stream of events. Here's an example for our Game.
Notice that some of the nullable values or default cases have disappeared. That should help us simplify some of the code previously needed to handle such cases. Other than that, if we managed to keep the same public interface while converting extension properties and functions to instance equivalents, hardly any other code should be required to change.
Deriving the state will usually follow the same pattern. It's a left-fold of previous events.
What's going to be different with each model implementation is the applyEvent()
function. Its job is to take the state we have so far and calculate the new state by applying the next event.
As we can see in the above example, the GameStarted event is used to initialise the Game state as it should be the first event that happened. All the other events only modify the previous Game state by changing a subset of its properties.
As long as the first event in the stream is GameStarted, our applyEvent() function will always return a non-null value. We ignore an event if that's not the case. That's what those ?.
safe calls will take care of. It's not our job to validate anything at this point. Any validation is done when we execute the command. If the event was generated it must be possible to apply it to the state.
To make this work, we will need to update our tests to build the state and pass it to tested behaviours instead of events.
Initial state
The initial state is passed as the initial value to the fold function. In previous examples, we used null
for the initial state. In many cases like ours, it's perfectly fine. In other cases though, there might be a valid initial representation of a state that's not null. Let's now consider how our game implementation would look like with a non-nullable initial state.
Instead of having null
for the initial state, we will now have it explicitly modelled.
We now have a sealed interface for the Game
with two implementations - NotStartedGame
and StartedGame
.
The advantage of this approach is its clearness. Furthermore, if there's a natural initial state for the thing we're modelling, it could have some properties initialised. That's not the case in our example. We could possibly initialise attempts
or outcome
, but all the other properties are provided when the game is started.
While applying events, we can now make a clear distinction between the two cases of not-started and started games. It becomes a bit more verbose, but also more explicit. We can easily see the scenarios when the game is not updated in response to an event. Also, safe calls are gone since now the game cannot be null.
Checks for the state become more explicit too.
Notice that in the above example, we take a Game but we return a StartedGame. Courtesy of Kotlin smart casts.
Once we checked if the game was started we can rely on this fact and pass it down to other functions.
At the end of the day, it's very similar to using a nullable state. Except that our initial state has a name.
The question we should ask ourselves is whether having a non-nullable initial state is so much different to using null
that it warrants introducing additional types. Probably not, if we consider our game example. However, it might be a useful technique for complex models.
Finite state machine
Once we realise that our previous example actually models a state machine with two states, we can expand it to model all possible states. Let's continue with our game example to explore how it changes the model.
As we can see on the above diagram, the game can be in one of four states: NotStartedGame, StartedGame, WonGame, and LostGame. NotStartedGame is our initial state, while WonGame and LostGame are terminal.
Here's how Mastermind game states can be implemented in Kotlin.
While applying events we now need to handle the two new states - WonGame and LostGame in response to GameWon and GameLost events respectively.
WonGame and LostGame states are terminal so we won't be applying any events once the game reaches one of these two states.
A nice side-benefit of all this is that we could remove some functions from the StartedGame
, like isWon()
or isLost()
. Instead, we can now use types to check the state of the game. The outcome
property could also be removed from StartedGame
for the same reason.
This is even more explicit than the previous model.
Command handlers
What’s left to consider is how to build the state to be used during command execution so it could participate in decision making.
What we'll get from an event store is a stream (or a list) of events. If the command handler expects an instance of state we will need to build it first.
Here's a generic creational function that builds an event list-based handler from a state-based handler.
To create the new handler we will need to pass the one we want to adapt (execute
), a function for applying an event to the state (applyEvent
) and the initial state factory.
To create a handler for a non-nullable initial state we will need to change the initial state factory.
Such an adapted handler can be invoked with events directly. Events will then be used to rebuild the aggregated state that is in turn passed to the model's command handler (our execute
function).
Summary
We considered several ways to implement state to be used in decision-making in an event-sourced domain model. We're equipped with a varied range of tools suitable for different sizes and maturity of problems, from deriving the state with stream extension functions to employing finite state machines.
Next time, we'll take a small detour to experiment with converting our solution to be more object-oriented.
Resources
Posted on November 10, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.