Last updated on 13.01.2019
Hello! This is a series of posts about Aecor — a library for building eventsourced applications in Scala in purely functional way. If you haven’t yet, you might want to go though Introduction and Part 1 first.
In this post we’re going to:
- peel off the layers of
EventsourcedBehaviour
class we’ve seen in the end of Part 1; - fine tune the behavior of our Booking entity;
- learn a new monad transformer along the way.
At some point in writing this post I thought of splitting it in two parts, but in the end decided not to — if it gets too long before we launch something, people might get bored. So this post is a very long read again. I may be a good idea to read it in chunks.
But at least, in the next part it’ll be all about running Aecor behaviors, as planned.
Part 2.1. Understanding Eventsourced Behavior
Previous time we defined Booking entity behavior using plain Scala and a pinch of MTL type classes from Aecor.
It was looking good, so we began to wrap it into something Aecor can launch, which led us to this bit of code.
To put it shortly, it does two things:
- composes pieces of logic we wrote into a single coherent behavior instance;
- specializes our
MonadAction
-ish effectF
.
It will take us several steps to completely understand what’s going on. Let’s start with the effect part.
Meet ActionT
As you can see, it’s no joke. So what is ActionT
?
The name hints it’s a monad transformer. Monad transformers were invented more than 20 years ago, and these days serve as building blocks to run programs, written using MTL-style (with type classes like MonadState
, MonadError
, etc.).
As you probably guessed already, ActionT
is used to run programs, defined in terms of MonadAction
. Let’s do a quick recap of what MonadAction
should be able to do (from Part 1):
- rely on some state of type
S
to make decisions. We can alsoread
this state.- Produce (or
append
) events of typeE
as a reaction to commands.- Return some kind of result to the caller of the action.
And now directly to the definition of ActionT
:
So, first thing I noticed when I initially saw it is S, E, A
in type parameters, which is pretty cool, because “Aecor” translates as sea or ocean from Latin.
Now back to the features we want from ActionT
. The unsafeRun
signature is pretty cryptic so let’s break it up. It’s a function of three arguments:
S
(S, E) => Folded[S]
Chain[E]
You should remember the Folded
type from the previous post. Given these three arguments, unsafeRun
function returns a value of type F[Folded[(Chain[E], A)]]
.
For those of you who haven’t heard of Chain
yet, it’s a collection from cats that does both append and prepend in constant time. You can freely replace it with List
for the purposes of this series.
Let’s draw what we’ve got and solve the puzzle:
Now, given what we know about MonadAction
and by substituting type parameters with what they represent, it should be rather clear:
“Give me initial state S
, a way to fold events E
into S
and a log of already happened events, and I will run some action that will return an A
and maybe produce some more events. So in the end I’ll return new a new (possibly amended) event log and the A
. All of that happens under effect F
.”
So it’s actually the same command handler in disguise! There’s a twist though. It’s a composable command handler, by which I mean you can chain them one after another.
It’s possible because of the third parameter — the log of already happened events. Of course, it’s not the whole history of the entity. These are the events that have happened up to this moment of executing the handler.
A picture would be much more expressive in explaining this:
So we can compose actions (or command handlers) into chains, where each individual stage takes the event log from the previous stage, appends it’s own events and passes it down to the next stage.
This is done for two reasons:
- each action can run this log through the folder to get current state (which is usually needed to do anything meaningful);
- we need the full log produced by all actions when the whole sequence is complete.
As you see, there are strict rules to how event log Chain[E]
should be handled, that’s why the ActionT
constructor is private. Not every unsafeRun
function would work.
On the other hand, initial state and folder function don’t change throughout execution of the whole chain. They can be completely arbitrary. The public run
method on ActionT
confirms all of this: you can run your action with any initial state and folder, but you have to start with an empty log.
The M word
After long explanations like this people often tend to say the M word. I’m no exception today.
So first of all, unsafeRun
returns an F[_]
. And then we somehow expect the next action to use the stuff from inside the F
. Which requires a F
to be a Monad (and which is actually it’s whole point).
But the composition of actions itself looks monadic as well, and it actually is! So each transition on the picture above is just a call to ActionT.flatMap
!
It should all click now for those who are familiar with transformers. Indeed, MonadAction
is a Monad
, so if we want to run MonadAction
programs with ActionT
, it has to be a Monad
as well. And, like other monad transformers, it’s only a monad when underlying effect F
is itself a monad.
As you might remember from Part 1, we found that MonadAction
is quite similar to a combination of Reader
and Writer
. ActionT
confirms this similarity: it’s indeed a Reader
of initial state S
and a Writer
of events E
.
I hope this “let’s connect the dots” deviation was not too boring for those of you, who got it all straight away. The main takeaway here: ActionT
is the engine for MonadAction
programs (effectively command handlers), that accumulates produced events along the way.
As an Aecor user, you won’t actually have to deal with ActionT
directly, but it surely helps to know, how it works.
EventsourcedBehavior
Now it’s a good time to look at EventsourcedBehavior
. As I said, it just gathers the pieces of behavior into one coherent thing:
If it’s the first time you see an abstraction over a tagless final algebra, you might feel this way (I surely did):
But the confusion goes away very quickly. Such type parameter expects something of a shape Algebra[F[_]]
, for example our Booking
behavior algebra would fit nicely as M
in EventsourcedBehavior
.
So what’t inside?
Two things we’ve just discussed in the ActionT
section: initial state S
for the behavior and a way to fold events E
into the state (the folder function). So let’s focus on the actions
portion.
So we have some algebra M
and some “raw” effect F
. EventsourcedBehavior
interprets algebra M
in a more complex effect, which is ActionT[F, S, E, ?]
. Let’s try these mechanics by hand with a subset of our EventsourcedBooking
algebra from Part 1.
We substitute effect I
(letter was changed intentionally) with an effect ActionT[F, S, E, ?]
for some lower-level effect F
:
This code is not what you get exactly, it’s purpose is to just make the point. I removed the implicit requirement in the result, because it’s now satisfied automatically via the MonadAction
instance for ActionT
.
Well, not exactly — we didn’t touch rejections yet (I simplified the requirement to MonadAction
for now). But, putting them aside, we’re good — ActionT
provides everything we need. And if we generalize back:
For any algebra
M
that requiresMonadAction
, putting it insideEventsourcedBehavior
satisfies that requirement.
Cool, so actions
is just a named set of behavior specific command handlers. Now we need to tune something here so that it can deal with rejections.
EitherK
The default way to embed errors into arbitrary effect F
is well-known: EitherT
monad transformer. It turns F[A]
into F[Either[Error, A]]
, which allows to embed errors into the left channel of the Either
.
Aecor faces a more challenging problem, though. For EventsourcedBehavior
to support rejections, it has to be able to transform an arbitrary algebra M[_[_]]
into some M'
so that for every F
, M'[F]
can embed rejections.
Sounds nuts? It’s actually simpler than it sounds. Let’s look at an example.
Given behavior like this:
we have to wrap all the F
‘s into EitherT
, so that every method in the algebra can embed rejections, like this:
One more time, but just the diff:
If we try to generalize it into arbitrary algebra M[_[_]]
, we need this kind of transformation (hope you got used to M[_[_]]
thingy a little bit):
1 2 |
// given any effect F[_] M[F] -> M[EitherT[F, Rejection, ?]] |
And that’s the essence. This way you can take any tagless final algebra and “teach” it to work with errors.
To save some typing and get better type inference, Aecor has EitherK
:
Please, note, that cats library has it’s own EitherK class, which is a completely different thing. Whereas
cats.data.EitherK
is just a coproduct, where each channel is under some effect,aecor.data.EitherK
is a sort of higher-kinded monad transformer.Naming the latter
EitherTK
or something alike would resolve the name clash, butEitherK
is simpler to type and pronounce. I haven’t yet seen a context where both would be used at the same time, so this ambiguity doesn’t feel like a big deal so far.
EitherK
really does what is says. Writing EitherK[Algebra, Rejection, F]
would give you an Algebra[EitherT[F, Rejection, ?]]
.
Let’s apply this to EventsourcedBooking
and, out of curiosity, see what effect type we’ll end up with.
Please, don’t flip your table and leave: you won’t have to type those signatures when using Aecor. We’re just unwrapping it all to get a deeper understanding.
In essence, this is an algebra of command handlers (powered by ActionT
with respective state and event types), that can fail with errors of type BookingCommandRejection
, (capability provided by EitherT
transformer). This combined effect gets a valid MonadActionReject
instance, and completely satisfies the initial requirements for EventsourcedBehavior
.
Looking at it again
Remember where we started? We assembled EventsourcedBehavior
for Booking algebra:
Now it should make sense to you. We’re building an ActionT
-based behavior with rejection support via EitherK
.
The only missing bit is optionalRejectable
smart constructor, which is nothing more than sugar:
- it allows us to use
BookingState.init
andBookingState#handleEvent
without lifting result toOption
; - it requires an
EitherK
based algebra, which helps to drive the type inference, so that we only need the outer level type ascription.
Here it actually makes a lot of sense to download the ticket-booking project (or aecor itself), open it in IDE and click through definitions to see how types match up.
Phew… I bet this was intense and took a bit of energy. It’s a good time to take a break before we look at how we can tune an EventsourcedBehavior
.
Part 2.2. Tuning EventsourcedBehavior
So now that we’re somewhat comfortable with EventsourcedBehavior
, let’s see how we can modify it to our needs.
Event metadata
As I mentioned in Part 1, it’s beneficial to separate essential event data from generic metadata. Especially if the latter doesn’t participate in making decisions within the behavior. Let’s see how we can do it in Aecor.
Given behavior
that we just constructed, we can call behavior.enrich
to get a new behavior that will automatically enrich every event with metadata we specify.
Let’s say we want to store a timestamp along with each event. For enrichment to work we need only one thing — a suspended getter in F
:
What you get is the same behavior, but with different event type: instead of plain BookingEvent
, you get events of type Enriched[EventMetadata, BookingEvent]
, which is just a simple product:
For every produced event Aecor will execute generateTimestamp
and put the result into the Enriched
envelope.
One small thing is missing though. enrich
requires that the algebra baked inside EventsourcedBehavior
has a FunctorK
instance. In plain words it means, that for such algebra M
, we can at any time go from M[F]
to M[G]
given a natural transformation F ~> G
.
Providing FunctorK
instance is mostly a mechanical process, and thanks to cats-tagless project we can get it for free. We just have to annotate our tagless final algebra with @autoFunctorK
:
That’s it. It was really simple, and we get metadata supply without polluting our clean events with irrelevant things. Power of composition is unlimited 🙂
Do you even lift, bro?
Now a plot twist. The whole team realizes we forgot to add booking expiration functionality.
Well, it’s not that bad, we’re still working out the behavior. So on the entity level it should be rather simple (you might want to refresh the Booking algebra we defined in Part 1).
First, we need an expiration event. Also, when booking is initially confirmed, we should store the exact moment in future when it will expire (if not paid or canceled by that moment):
Second, we need to make corresponding adjustments in the algebra: add an expire
action and tweak confirm
action to receive the expiration deadline.
The expiration deadline will come from an external ticket management system, so for the Booking behavior it’s just an argument. We’ll also add an expiresAt: Option[Instant]
to the BookingState
— we’ll need it to validate any attempts to expire the booking too early.
Last thing to do is to implement expire
action in the EventsourcedBooking
version of Booking algebra. And this is where we’ll get stuck.
To check that expiration doesn’t happen to soon, we’ll need to get current system time, and check that it’s already past the expiresAt
deadline. Getting current time is a side-effect, so we’ll need some kind of effectful clock. We’ll pick one from cats-effect:
But if we then try to work it all up from here, we’ll have a hard time finding a Clock[I]
instance. As we remember, I
is a very special effect — it supports command handlers that produce events.
But we need just a simple clock, is there a way to avoid all of this additional complexity?
For sure. If we need a simple effect, let’s just add it:
Good. But there’s another problem. Once you execute your clock, you get an F[Instant]
. But you can’t just flatMap
it into an I[Something]
— F
and I
are completely unrelated effects. If there was a way to go from F[Instant]
to I[Instant]
, then everything else would work as it did before.
It turns out that there is such a way, and it seems logical. After all, F
is a much simpler effect, and the more powerful I
should be able to embed F
values.
For such behaviors Aecor provides MonadActionLift
and MonadActionLiftReject
type classes. These are just extensions over corresponding type classes we already know:
This is all we need. Now we can liftF
the result of our clock into I
, and everything will work again. Let’s see the final implementation:
Types will work their way up now. To see how it looks as a whole, please refer to the repo.
One may ask here:
So we embedded a clock into our behavior. But the same way we could embed any kind of effectful service, right?
Seems like too much power, where’s the line?
A fair question. Although it’s possible, I would keep it to simple local side-effects like Clock
or Logger
. Embedding something more complicated doesn’t make a lot of sense — if it’s a database or external request, you don’t get any additional atomicity guarantees anyway. But the downside is significant — you clutter your behavior with some logic, that could be executed elsewhere (and the results would be then passed to behavior as simple arguments).
Still, the possibility is there, so if you make a really good case — go for it! I can imagine some external validation being implemented this way. In my opinion, as long as this external service doesn’t populate your events, you’re fine.
Time is out
Well, not really. But this is the last tweak I’ll cover today. It’s a really simple and neat example of how powerful these effect-polymorphic behaviors are.
It’s not really relevant in this case, but let’s say we have a behavior that can take long to handle commands. Maybe it does some really heavy calculations or goes into database for validation purposes.
In this case we’d want to limit the maximum response time and get a timeout if the command processing takes more than 2 seconds. With a less composable solution we’d have to bake it inside the behavior, or handle it on the outer level.
With Aecor you keep your original behavior intact and just run it through a natural transformation to get a modified version. This requires mapK
, and EventsourcedBehavior
is a FunctorK
as long as underlying algebra M
is a FunctorK
too.
Let’s define a natural transformation that implements a timeout cutoff:
We leverage some goodies from cats-effect here, namely Concurrent.timeoutTo
. This natural transformation takes any Concurrent
effect F
and produces an effect of the same type F
, where any action that takes more than 2 seconds will raise an error. We can do that since Concurrent
extends MonadError
.
The only thing left to get a timed-out behavior is to run the original behavior through the given natural transformation:
And that’s it. Pretty concise, isn’t it?
Conclusion
This was a really dense post with a lot of code to crunch. I really appreciate the time you spent reading and hope you extracted some value out of it.
Next time we’ll finally launch our behavior on a real cluster with a real event journal. Stay tuned.
Thanks, very useful.
Aecor
deserves such detailed coverage.Liked the use of
BookingCommandRejection
in both theEventsourcedBooking
andBookingEndpoint
.There is conceptual overlap between the eventsourced algebra and the microservice endpoint – good to see implementation reflects some of that.
Plans to take this further?
Hey, Nick. Thanks for kind words!
As for connection between aggregate and endpoint. There definitely can be connection, but most of the time only a subset of aggregate handlers would be accessible through some public endpoint. Some of the handlers would be internal and be triggered by processes within the system.
So I think you could probably come up with some tool that will generate endpoints for an aggregate, though you would have to be able to pick specific methods. And various concerns like validation and authorization would have to be handled somehow, so it looks like a really challenging task 🙂
There are a few frameworks for defining endpoints that also use a similar _”
behavior algebra
first”_ approach and this suggests an opportunity to unify somehow.http://frees.io/docs/core/algebras/
http://pepegar.com/hammock/algebras.html
Also, there is a need to expose
eventsourced
nature at the level of the endpoint, not just at the level of the aggregate.Indeed, _”most of the time only a subset of aggregate handlers would be accessible through some public endpoint”_ but am wondering how to improve my thinking of composability across remote calls.
The context I am thinking in is _”The better composability of typed, pure FP once again becomes a significant lever, because process boundaries no longer destroy composition.”_
Source: https://pchiusano.github.io/2017-01-20/why-not-haskell.html
I understand your point. Although a desire to expose eventsourced nature outside seems contradictory to the whole idea of composition.
Composition is about hiding unnecessary detail. Generic behavior is more abstract and thus more composable – you can use it in more contexts, than a more specific eventsourced behavior. Maybe I just didn’t get your idea properly 🙂 Could you come up with an example, where you would benefit from knowing about eventsourcing at the endpoint level?
Unison is an exciting project with an ambitious goal. I’m really curious how it plays out. Aecor has different focus though — it’s all about eventsourcing within a single (maybe distributed) service. And in my opinion it composes quite well in this context.