In the previous post, we looked at the benefits of a domain model implemented in a purely functional code.
This time, we’ll consider how it might work in practice by applying the event sourcing pattern to a functional domain model.
As it turns out the two go very well together.
Event sourcing
The idea behind event sourcing is to:
Commands in, events out
At the fundamental level, we only need one function for each aggregate root to handle its behaviours. The function will calculate a sequence of events in response to a command. Command is an intention to trigger a behaviour.
Let's call this function Execute since its job is to execute the command.
Note: I chose Execute
as the name for the function following one of the naming strategies for aggregate methods found in "Learning Domain-Driven Design" by Vladik Khononov.
History is not forgotten
As we execute commands for any given aggregate, events will be appended to a journal (event store). Past events will be needed to handle future commands for that aggregate.
For example, a game cannot be played before it is started or after it is lost. Or, once an order has shipped it can no longer be amended.
We will look into the history to make decisions.
Therefore, the Execute function will also need the past events. That list will be empty when the first command for a given aggregate is executed.
Here's the updated type definition of Execute:
Protecting invariants
That's going well, but what about error cases?
As we agreed in the last post, we want to be explicit about domain errors. That's why we'll make them one of the possible outcomes:
The Execute function will need to either return an error or a non-empty list of events:
Note: We use Arrow for typed errors, specifically the Either
and NonEmptyList
types. However, any type-safe error-handling library would work. Result4k is a nice alternative to Arrow.
The matter of state
As we defined it so far, the Execute function is the minimum we need to implement an event-sourced domain model.
Here's an example of a further unidentified game pending the implementation:
typealias Game = List<GameEvent>
fun execute(command: GameCommand, game: Game)
: Either<GameError, NonEmptyList<GameEvent>> = when(command) {
is JoinGame -> TODO()
is MakeGuess -> TODO()
}
Let's assume we don't want to allow to make guesses once the game is won. The game is considered won when the GameWon event is published. In such a case, any MakeGuess command should be rejected with an error.
As we suggested before, we will need to look at the history of events to make decisions. Having the list of events passed to the Execute function we can scan it for facts:
events.filterIsInstance<GameWon>().isNotEmpty()
Furthermore, we can name any concept by defining an extension function for it:
private fun Game.isWon(): Boolean =
filterIsInstance<GameWon>().isNotEmpty()
Remember the Game
here is actually a List<GameEvent>
.
Thanks to extension functions and our previously defined type alias for the Game (as a list of events), it becomes very natural to use:
if (game.isFinished()) {
return GameAlreadyFinished(gameId).left()
}
// Note: `.left()` wraps the result in an `Either.Left`
// that represents the error branch.
Just as if we had the Game class defined.
It is just a list of events though.
It will work rather well for short streams of events. However, scanning long streams might get expensive, especially if we make decisions based on a lot of factors. For such scenarios, we can introduce an optimisation in the form of a computed state (projection).
The entity's state can be derived from its history of events.
The difference between using a computed state and scanning events history is that when we reconstruct the state object every event is applied only once to update the state. Then, we can access it however many times we need. In the case of event scanning, the scan is performed every time we need to access a bit of information.
Note: We can go a long way without having to introduce a computed state if we treat the history of events as the state and scan it for information as shown above.
I often use this technique as an intermediate step. It helps me to learn what the properties of the state object should look like. It sometimes turns out to be a different view than I thought of before the implementation.
Execute with state
Let's redefine the Execute
function one last time to account for the possibility of the state being any kind of type (not just an events list).
Here's the final revision of the Execute function type alias:
This way we can still use a sequence of events as a state:
fun execute(command: GameCommand, game: Game)
: Either<GameError, NonEmptyList<GameEvent>> = TODO()
typealias Game = NonEmptyList<GameEvent>
private fun Game.isWon(): Boolean =
filterIsInstance<GameWon>().isNotEmpty()
but we can also decide to implement it as a dedicated data class:
fun execute(command: GameCommand, game: Game?)
: Either<GameError, NonEmptyList<GameEvent>> = TODO()
data class Game(val isWon: Boolean)
The state is made nullable as it won't exist when the first command is executed.
Alternatively, we could model the initial state as a special type. The choice depends on the use case. Sometimes it's natural to have a dedicated initial state object; while in other cases it makes more sense for the initial state to be null
(as no natural initial state exists). As we know, in Kotlin we don't need to be afraid of nulls.
State reconstruction
We will need a way to reconstruct the state with events we got from the event store so that we can pass it to the Execute function.
Instead of scanning through the history of events to access every bit of information, we will instead build the state by applying events to it one by one.
To calculate the state, we will start with null
(or other initial state), and fold events with an applyEvent
function.
The applyEvent
function, or simply Apply
, should take the state, an event, and return a new state.
In other words, it will calculate the new state based on the previous state and an event.
Here's the type alias for the Apply function:
Here’s how it could look like for a further unidentified game:
fun applyEvent(game: Game?, event: GameEvent): Game? = when(event) {
is GameStarted -> Game(event.secret, Outcome.UNRESOLVED)
is GuessMade -> TODO()
is GameLost -> game?.copy(outcome = Outcome.LOST)
is GameWon -> game?.copy(outcome = Outcome.WON)
}
Folding events over the state means that the Apply function is called on the initial state and the first event to calculate a new state, which is in turn passed to the next invocation of Apply with the next event. This process is repeated until we run out of events and return the final version of the state.
All together now
The command will usually come from a user in one way or another (sometimes from other systems, in reaction to events or other commands etc).
Events will be loaded from an event store.
These two are not compatible parameters for the Execute function if we decide to use a computed state.
We will need to convert:
To:
The conversion of one type to another isn’t too complicated. We can implement a creational function that takes one version of Execute
and returns the desired one:
fun <COMMAND : Any, EVENT : Any, ERROR : Any, STATE> handlerFor(
execute: Execute<COMMAND, STATE, ERROR, EVENT>,
applyEvent: Apply<STATE, EVENT>,
initialState: () -> STATE,
): Execute<COMMAND, List<EVENT>, ERROR, EVENT> =
{ command, events -> execute(
command,
events.fold(initialState(), applyEvent)
)}
Apart from Execute
we will also need to pass Apply
to fold events and build the state.
Additionally, it’s just too easy to support any initial state, not just nulls. That’s the job for the initialState
function.
Inspirations
Before I started a project with this approach I did a lot of reading on functional programming techniques for event sourcing. One notable read I remember is Functional event sourcing decider by Jeremie Chassaing. An attentive reader will notice that what I described here comes very close to Jeremie's decider pattern.
I didn't set out to implement the decider pattern, but it kind of happened anyway. Kind of. Not really. Maybe?
Depends on how far you'll allow us to diverge from the pattern.
The decider pattern is made of the following parts:
class Decider<COMMAND: Any, EVENT: Any, STATE: Any>(
val decide: (COMMAND, STATE) -> List<EVENT>,
val evolve: (STATE, EVENT) -> STATE,
val initialState: STATE,
val isTerminal: (STATE) -> Boolean
)
The approach we've taken is:
class Handler<COMMAND : Any, EVENT : Any, ERROR : Any, STATE: Any?>(
val execute: (COMMAND, STATE) -> Either<ERROR, NonEmptyList<EVENT>>,
val applyEvent: (STATE, EVENT) -> STATE,
val initialState: () -> STATE,
)
Apart from naming conventions (Execute/Apply vs Decide/Evolve), there are a few other differences:
- I accept
null
as a valid initial state. - I’m in favour of explicit domain errors rather than using exceptions.
- I treat evolve and initial state parts as optional.
- I have not found the need for the terminal state (yet?).
Now go and read Jeremie’s article or listen to one of his talks.
Summary
We have applied the event sourcing pattern in a functional domain model.
Next time, we will discuss an example implementation before moving on to the outer layers of the application.
Top comments (1)
github.com/fraktalio/fmodel