In this post we’ll explore entity behaviors in general, and how to make eventsourced behaviors with Aecor. I’ll also delve into design practices and try to answer arising questions, so grab a coffee — it’s not a quick read 🙂
Part 1. Defining entity behavior
As we agreed in the introduction, we’re building a ticket booking system.
Booking tickets is the core domain of our imaginary business, and with all this complexity and inherent temporality, “Booking” entity is a good candidate to be eventsourced.
But why exactly Booking? How does one come up with this decision? Let’s stay a little bit on this topic.
- has a distinguished identity, that allows to differentiate two instances of an entity, even if all their attributes are the same
- usually obey some form of lifecycle, according to business rules.
One can easily define several entities in a ticket booking context, and Booking is what first comes to mind:
- it must have some unique identifier, so that a client can refer to it at any time (identity)
- as the booking process goes forward, it goes through several distinct states (lifecycle)
So we selected Booking as one of our entities. This alone doesn’t imply event sourcing — in classic DDD entity is backed by a regular CRUD-ish repository.
But if we see value in eventsourcing some part of the system, entities are usually a natural fit to have a consistency boundary wrapping them around.
Eventsourcing an entity (or several entities, wrapped into an Aggregate) usually gives the best trade-off between granularity (which gives scalability) and consistency, meaning that most of our business invariants can be checked within single consistency boundary.
So we decided to eventsource our booking entity. Time to define some behavior for it!
First step, which actually doesn’t require Aecor or any other library, is a Tagless Final algebra for your entity. Let’s put it like this:
If you’re not familiar enough with Tagless Final, there’re lots of good posts on the web. I can recommend this one by @LukaJacobowitz, or my own Writing a simple Telegram bot with tagless final, http4s and fs2.
So what do we see here that our booking can do?
- We can place a booking for 1 or more specific seats in a concert on behalf of a client.
- Booking can be confirmed, which means, that seats are reserved and prices are determined. We define an explicit confirmation step here, because actual concert data management and seats reservation is done in another system. Confirmation is going to happen using asynchronous collaboration with that system.
That system also manages pricing, so when booking is confirmed, seats become tickets — in our case a ticket is just a seat with price attached.
- By the same token, if something goes wrong (e.g. seats are already reserved), booking is denied with a reason.
- Client can cancel the booking any time.
- Receive payment is an obviously important lifecycle action for booking.
- And our entity will expose some parts of it’s internal state, namely status and tickets (optional, because there’re no prices until the booking is confirmed).
Just several lines of code, but quite a bit of thought and effort. And also questions! I’ll try to answer ones that most probably arise at this point.
This algebra definitely looks like something with internal state. Why so?
It’s true, and for reasons:
- We focus on behavior. Internal state that will fuel it is secondary, and we don’t want to tie the behavior algebra to it.
- When some other component calls an action of this behavior, it shouldn’t be bothered with booking internal state either.
I usually think of it like this: an instance of this Booking algebra would represent a specific booking entity instance at it’s current state, and the methods are actions that you can perform with that instance.
F[Unit]all over the place? And where are the errors? You can’t pay for a denied booking, for example.
Unit here represents some kind of “Ack” response, meaning that the action succeed. Booking will probably change inside, but we don’t care. Returning
Unit in this case is very common in Tagless Final.
As for errors — in good traditions of MTL we delegate error handling to our effect
By the way, at the moment it looks like most TF algebras out there, where
F is going to be something like
Task in the end. Spoiler: it won’t be so when we get to eventsourced behavior.
If these actions are for a particular booking, what is
placedoing here? Don’t we create a new booking by placing it?
This is an interesting one. When you do traditional CRUD, the creation of entity instance is separate from any kind of logic it might have (or not have).
But if we move completely into behavior land, then there’s definitely some kind of business action that brings the entity into existence. In our case this is
place action. It’s a an integral verb of our domain and a part of the entity lifecycle, so we treat it accordingly — it belongs to the entity algebra.
Behavior actions, MTL-style
I hope we’re ready to move forward and finally unpack some Aecor typeclasses. Let’s take a look at them.
The core one is
It provides basic building blocks for actions. Aecor action describes how an entity reacts to incoming commands, which makes it very similar to command handler concept. As signatures might have suggested you, actions:
- rely on some state of type
Sto make decisions. We can also
- Produce (or
append) events of type
Eas a reaction to commands.
- Return some kind of result to the caller of the action.
So any effect
F that can do these things can be used to describe actions and thus have an instance of
We will also need errors. In the context of handling commands an error means that command can’t be executed for current state of the entity. For example, one should not be able to pay for a denied booking. In this case we say that
receivePayment command is rejected, and the action resulted with rejection.
Aecor provides a more powerful version of
MonadAction, that can work with errors. It’s called
It’s related to
MonadAction in the same way to how
MonadError relates to
Monad. Usually, your entities would need rejections, but sometimes there’s no such need — this is where you can get away with a simpler
Before we implement our actions, we’ll have to agree on
R types for our eventsourced booking.
Implementing event sourcing is inherently harder than more traditional state-based approaches. One of the reasons is that in addition to state you will need events (and in our case also rejections).
Mining proper events from the domain is a big topic in itself. Let’s say we already had an eventstorming session with our domain experts and came up with the following events:
BookingSettledare distinct events, because some bookings are free and can be settled without payment.
Notice, that we’re back to no-dependency mode: these events are completely arbitrary and library agnostic — no marker traits or similar hacks. Maximum composition.
Also, we don’t put any identity information or metadata (e.g. timestamps) here. Aecor provides a way to decouple business-related data from metadata to make events cleaner. We’ll see later, how you can enrich your events with metadata. We’ll discuss identity soon as well.
Next, we’ll need our entity to keep some state inside. We should not fall into a trap of thinking database schemas here. The purpose of this state is not to map into tables or provide efficient queries — it’s part of your domain model, so it should:
- be readable and use ubiquitous language;
- be rich enough for expressive command and event handling;
- support the whole entity lifecycle.
We’ll use the following state for our entity:
ticketsis optional, because we don’t have seat prices for the whole life of the booking — we get them with confirmation. A more typesafe way to encode this would be to put a non-optional
ticketsfield in all statuses where the tickets have to be in place. Here for simplicity we just put an option into the state root.
And again — our state is totally library agnostic.
Identity in state and events
A fair question here would be:
You say a lot about identity, but where the hell is the
This is a neat idea I first heard from Denis Mikhaylov. It says that in general, entity should not need identity information to handle commands. You definitely need some kind of identifier to route a command the the correct entity instance. But after that business logic doesn’t usually care.
Moreover, when it appears that chosen identifier is still required for business logic, you most probably can decompose it into two parts: pure identity and something that is required for command handlers to work. Then you move the former out of your events and state, keeping only the latter.
I’ve implemented and seen this idea in action, and I find it awesome. Separation of concerns all the way down. Answering the question — we’ll definitely see
bookingId later, but it’s not relevant for our behavior.
I won’t spend too much time on rejections. Simple enum is usually enough, but nobody stops you from enriching your rejections with some data. Here’s what we got for booking command rejections:
We’re ready to implement actions for our eventsourced behavior. We’ll start by requiring our effect to be a
Our ADT’s from previous sections took their respective places, with one quirk: state is wrapped into
Option. This is where we get back to the trade-off of having
place verb in our behavior algebra. Until the booking is placed, there’s no trace of it in the system, and hence no state.
It’s a common thing in event sourcing: very often there’s some kind of initial event that moves the state from
Some(...). At this level we have to accept this and express it in our types.
Let’s walk through this code:
MonadActionRejectDSL into scope
readto get current state of this booking entity
- If something is already there, it means that this particular booking was already placed and we can’t re-place it again: reject the command.
- If it was not placed, we perform some validation an either
rejectthe command or
Congratulations, this is our first command handler!
Aside on MTL.
Monad. This gives us a lot of power in defining out effectful actions, especially when other effects come into play (we’ll see an example later).
MTL fans could have noticed, that
MonadAction[F, S, E] is very similar to a combination of
MonadReader[F, S] and
MonadWriter[F, E]. Rejections add up to
MonadError[F, R]. Notable exception is
reset combinator, which adds a remote flavor of
MonadChronicle: it allows to drop all the accumulated reactions and start over from a clean slate.
All of this is not accidental — it’s just the nature of command handlers. They have to read state, write events and raise rejections. So
MonadAction could probably “extend” these mtl typeclasses… but so far no practical benefit was found and
Monad is just enough.
Let’s complete the actions for eventsourced booking.
Let’s walk through
confirm action. Others are pretty much similar.
confirmruns on existing booking and should be rejected for a booking that was not yet placed. This is handled in
statusmethod, that confirmation action calls into.
- After booking is confirmed, if tickets are free we can settle the booking immediately. Notice, how regular monadic combinators are used to do that.
- Sometimes the handler doesn’t have to do anything and just ack (e.g. double confirmation).
ignorealias is defined for a better readability in these cases.
Experienced eventsourcing practitioners would say that this is only half of the story. Our behavior produces events, but we haven’t specified how the state would change in reaction to these events.
It’s not a secret that eventsourcing is conceptually just an asynchronous
foldLeft on an infinite stream of events. Obviously, we lack a folding function for this to work.
Actually, given the optionality of our our entity state, it makes sense to define two functions:
- one for initialization, where we go from nothing to something;
- second for more regular lifecycle transformations, from one existing state to another.
We’ll define both on our
BookingState since folding events is one of it’s direct responsibilities:
Here we face an eternal problem of eventsourcing, which is handling illegal folds. Usually lifecycle implies that some events can only happen in particular states. For example, we shouldn’t ever receive a
BookingDenied event for a booking that has
Command handlers must hold such invariants, so seeing an illegal fold at runtime is a programmer’s error. It’s very hard to navigate this knowledge into the fold function. Especially in a way that compiler would allow us to write only folds that make sense and will actually happen.
It would probably require much more complex type signatures and totally different structure to pull that trick off. The payout is nice but is not worth the effort: for a properly designed aggregate of a normal size code review is enough.
Aecor provides a specialized
Option-like type called
Folded[A] to account for illegal folds:
You can see it wrapping the fold result in the functions we defined earlier.
⚠️ A timeless warning!
Never side-effect in your event handlers!
Always worth mentioning. Aecor is as explicit about it as it can be — everywhere it needs a fold function, it’s has to be without effects.
In Haskell that would be enough, but not in Scala. Just keep doing pure FP and you’ll be fine 🙂
Bringing it all together
Now we’re finally ready to wire it all up into something Aecor can launch, which is (you don’t say)
Oh well… I guess this is enough for now. It was a long read, and the signature above screams for a fresh head. So let’s call it a day, and dive into
EventsourcedBehaviour next time.
Please, post your feedback in comments and thank you all for reading!