Communicating aggregates

event sourcing, F#, DDD | March 19, 2018

Last post I talked about event sourcing in F#. I wrote some code that enables you to write functional aggregates that can take a command and produce some events as a result. But what can you do if you need your aggregates to talk to each other?

Let's see if I can come up with an example that I can use to illustrate what I mean. In my day job I work with a domain that is all about scheduling in a health care context (think appointments between patients and doctors within a hospital), so I hope you'll forgive me for seeking inspiration within that domain. Whether the example is realistic, is not the point. I'll just make it so, that I can support the idea I'm trying to convey.

Let's talk scheduling, first

I think I don't have to look very far within my scheduling domain to come up with a fitting example. Let's say, as a doctor I want to schedule an appointment with a patient I will be seeing. First thing I'll need is a schedule. I'll use this schedule to my own ends so that I keep control of when I see patients, and for what purpose. That's typically how doctors work. E.g. they'll see patients for consultations on tuesday, while they'll be doing surgery on thursday. I'll also use this schedule so that I don't have too many appointments in one day, and I'll put a limit on my working hours. Let's say I'll be doing consultations from 08:00 until 12:00, and from 13:00 until 17:00. A consultation typically shouldn't take more than 20 minutes. If I do a post-op consultation however, that will at least be 45 minutes.

Modelling the aggregates

An aggregate is defined by the consistency boundaries of the domain concept it represents. This means that it envelops the rules that need to be guarded so that you can't bring the aggregate in an inconsistant -meaning invalid- state. To identify the aggregates in our domain, we look for these rules and we try to find around which nouns they cluster. That's a strong indication that these nouns are probably good candidates to be an aggregate.

So what are these rules in my example? One obvious first one is that you can't schedule an appointment before 08:00, between 12:00 and 13:00 or after 17:00. In other words, you can only schedule within my working hours. Another one is that, depending on which type of appointment I'm scheduling, it can't be longer than 20 minutes for a consultation, or shorter than 45 minutes for a post-op consultation (and for the sake of the example, let's say that's an enforcable rule). I've also stated that I don't want my schedule to be too full, so we'll have to make sure that once all the available time has been filled, I can't schedule any more appointments.

I think an obvious aggregate candidate here is schedule. It looks like it's guarding the working hours rule, and the amount of time that's been taken. Another one is appointment type, guarding the rules around the length of an appointment. The final one is appointment itself, even though I haven't come up with any rules for it in this example. But you can probably imagine that that will come with its very own set of rules once you get to work with the appointment (rescheduling, cancelling,...).

Guarding the rules

If we want to schedule an appointment, we need to know if we're scheduling outside of the working hours, whether there's enough time left in in the schedule to fit the appointment, and whether or not we're within the time constraints of the type of the appointment we're scheduling. If we're violating any of these rules, we can't go ahead with scheduling the appointment. This means that our command handler, referring back to my previous post, should return a failure result rather than an event (or set of events). But how can it do that? The state of the appointment aggregate doesn't have the information in order to do this. In fact, since we're just scheduling it, we're really only just bringing the appointment to life, so it's state will be the initial state at this point. This means it doesn't have any rules of its own to guard yet. What we have to do, is ask schedule and appointment type to check whether we're breaking their rules.

A new type

We've only got two types to work with at the moment, a CommandHandler and an EventHandler. In this case, we don't want to do anything with schedule or appointment type to change their state. So the CommandHandler isn't useful to us here. What we need, is a QueryHandler. What does that one look like?

type QueryHandler<'state, 'query, 'result> = 'state -> 'query -> 'result

Look at that signature. It looks rather familiar, doesn't it? Looks a bit like a command handler, right? Except that it doesn't return a failure or list of events. It returns a query result (generics at work here, the result can just be what you need it to be). But what makes it really interesting is the fact that it takes a state parameter. This means that we can play the same trick as we did with the CommandHandler: use our event handler to fold the stream of events to get the current state and pipe that into our query handler:

Seq.fold eventHandler initialState events |> queryHandler

Now that we know this, how can our appointment command handler work with this? It takes a state and a command argument, and that last one seems like a good place to put this. This is what the command would look like:

type ScheduleGuardQuery = { ScheduleOn: DateTime; }
type ScheduleGuardResult = Ok | OutsideWorkingHours | NotEnoughTimeLeft

type AppointmentTypeGuardQuery = { Duration: TimeSpan; }
type AppointmentTypeGuardResult = Ok | InvalidDuration

type ScheduleAppointmentCommand = {
  ScheduleOn: DateTime;
  Duration: TimeSpan;
  Schedule: ScheduleGuardQuery -> ScheduleGuardResult;
  AppointmentType: AppointmentTypeGuardQuery -> AppointmentTypeGuardResult;
}

Next to some primitive types, our command can carry query handler types for the schedule and appointment type. You'll notice that I've left off the the state argument from the function signatures here, and just expect ScheduleGuardQuery and AppointmentTypeGuardQuery. This is because our appointment command handler doesn't actually know of any state of the other aggregates, making it impossible to pass it in from there. We should load the aggregates we need beforehand, using the event folding above, and then make use of F#'s automatic function currying to go from 'state -> 'query -> 'result to just 'query -> 'result.

Consistency?

It may be inevitable that we will need aggregate types to communicate with each other. However, as I've stated in the beginning of this post, an aggregate is defined by the consistency boundaries of the domain concept it represents. It is important to remember that although an aggregate can guard its own consistency, this cannot be guaranteed for the communication between aggregates. When performing these queries on other aggregates, the state of those other aggregates might have changed while we are performing our own logic. Query and guard methods like these can probably only be seen as a best effort. If rules need to be guarded more forcefully, you may have to look into the design of your aggregates, check whether you have correctly identified your boundaries, or there might be other corrective mechanisms in your domain that mitigate the need for hard rules.