Published on

Many faces of DDD Aggregates in F#

Authors
Attention!

This tale is a tiny contribution to the great F# Advent Calendar 2022.

Take your time and check other contributions too (of course, after reading this tale)!

Kudos! πŸ™πŸ»

This tale wouldn't be here if I didn't have conversations with borar.

We had a lot of discussions (or rather me asking tons of questions and learning from borar), especially on functional topics.

Thanks borar for your patience, your wisdom and sharing your thoughts with me!

Plan?

In this little tale, we are going to explore an interesting building block, coming from "tactical design" of Domain-Driven Design approach, called Aggregate.

The key aspect will be the language I personally love - F#. We will see (opinionated) way of representing this model for solving a particular set of problems.

To make it more "real" (i.e. close to the business), we are going to help our customer (we'll meet him soon!). So we'll be challenged with the real business problem!

So, what is our plan? Here is a list of topics we are going to sequentially visit:

This tale grew a bit, so I might divide it into smaller tales based on the bullet points above.

But for now my dear Reader, just sit comfortably in your chair, take a deep breath and immerse.

Problem?

Who has the problem?

Meet Tom.

(this is the time when we are simulating a vivid discussion with Tom)

"Hi, my name is Tom. I am an owner of a luxurious disco clubs. I have a problem with consistency on counting how many people are partying and limiting it. Can you help me?"
Tom, the businessman. Source

What do you think, can we help? Good that we paid attention to the language, used by Tom, which resulted in catching the important word. Let's try to ask a bit more about the details, maybe we will learn something?

"Briefly, we are testing new ways of reporting on how many people are having fun in our clubs. Club bouncers are responsible for counting, but they are not satisfying the hard requirements we are giving them. They need to secure the limits so that people wanting to enter think of it as a luxury."
Tom's business problem. Source

This sounds serious, isn't it? Seems like we need to do some analysis and try to figure out what it is all about with those bouncers. Shall we?

There are multiple ways of approaching such activity, but in this particular example we will try to practice DDD.

DDD 101: strategic and tactical design

Let me give an "extended" version of the DDD shortcut, I personally like:

Business Domain Driven Software Design

It is not a definition, but rather a formula, I find particuarly interesting.

A function, when fed with domain knowledge, should yield an understanding that might lead to the problem-adjusted software design.

Exploring the problem, its nature, its essential complexity, might bring us closer to suggesting a proper solution (or solutions) - so-called models.

Models are related to the accidental complexity, our approach for providing value by solving the problem (what's the difference between those two type of complexity? Please check "Essentially bounded, Accidentally unlimited").

From the helicopter view, we can divide "the design" into two categories: strategic design and tactical design (you can find more about it in the original "Blue Book" by Eric Evans).

Both of them consist of various techniques, building blocks, or methods that one can utilize to model different aspects of the problem space.

What is very important, strategic perspective should drive the tactical perspective, not the other way around (then we might arrive at Design-Driven Domain, one of the two evil sibilings of Domain-Driven Design - what is the second evil twin? Database-Driven Design)

But jumping into the tactical design without exploring and analyzing the strategic aspect might be dangerous.

DDD 101: the language

The essence of strategic design is the language, called Ubiquitous Language.

Talk to me in your language.
I will talk to you in your language.
Code will express your language.

In other words, we are going to capture the language of "the problem", that Tom speaks about. This language will be the central piece of our work.

Aggregate?!

I must admit I misunderstood this concept, or rather a building block, in 100%. The name might be a bit...Misleading?

I like to go down to definitions (I used the same approach in the tale "(Fr)Agile"), to check if we can refine the understanding.

an aggregate - definition

a cluster of domain objects that can be treated as a single unit.

Googling for a definition made me lucky because this one sounds interesting.

Especially the highlighted part.

One could ask: a single unit of what?

With my current understanding (this assumes it might change in the future so bear with me), it is a single unit of consistency (also referred to a unit of bussines transaction).

Wasn't Tom mentioning some issues with consistency?

Aggregate?! - "whoa there cowboy"

As it got mentioned before, if we look down into a further analysis of the "aggregate" word, its main purpose might be depicted as data aggregation.

As an example, we could take C# LINQ Aggregate method that is related to data transformation. Next examples might be SQL Aggregate Functions: SUM, MIN, AVG.

As information or data is mentioned, the understanding might orbit around entities or "bags of data structures".

But data is the secondary citizen here.

Aggregate protects (or enforces) the consistency rules. Such rules might be also called invariants.

The internal integrity is guarded by those rules.

One could ask: the integrity of what? This is hidden, taken away from the sight.

Hence there's another, supplementary building block - Aggregate Root - that is the interface to the underlying world. Typically Aggregate and Aggregate Root is used interchangeably to refer to the same thing - consistency rules guard.

Aggregate?! - "real" example

Working with abstract terms like "abstraction", "loose coupling" might be intimidating and I am a huge beliver for finding corresponding real world examples that are more "tangible" (thus tales like "Organization-Driven Design" and "Microoffices" vs "Officeolith").

Then, what could be possible example of an aggregate, in "real world"?

Any parents there?

Imagine a kid having a high fever - they need to get ibuprofen or paracetamol. You measure the temperature, note it down on the piece of paper.

Body temperature measurements for each hour, on a piece of paper. Source: generated using DALLΒ·E

There is a hard rule that the intake must have to comply with a particular time regime.

Where is the aggregate? A parent is enforcing ("protecting") this rule, so parent represents the aggregate here.

Taking into account what we just discussed, isn't the name "aggregate" misleading?

How could we name such a "guy"? What would you say for a bouncer?

A bouncer.

Problem Analysis

Let's go back to our friend, Tom. We still have some learning to do!

Hey Tom! Could you please tell us more about your problem? (let's pretend we have a conversation here).

(hint: you can interact with Tom in two different ways by using buttons below)

"Well, each bouncer is either guarding or having a break.Then a bouncer can report new people coming only when he is guarding.Each shift requires at least three bouncers.When scheduling a break for a bouncer, we need to ensure at least two bouncers are guarding.To keep our clubs luxurious, we are limiting the number of partying people to 100.So when new people are coming and there is more than 100 people, we don't allow entering to the club."

By carefully listening to Tom's speech, we found out some rules.

How to proceed?

  • all rules need to be satisfied
  • we need to suggest aggregate boundary and check if rules are satisfied
  • align to the language used by the Tom

Let's go!

Attention!

Note that this is a toy example and all the solutions are provided in order to show various perspectives.

I am totally aware that there are other ways of approaching this modeling task - if you have your preferred one, please share! πŸ™πŸ»

Solution #1: Object-oriented approach

Firstly, as a warm-up, let us start with "typical" OO way of modeling.

What a bouncer can do?

  • have a break
  • start guarding
  • report new people coming, only when guarding.

Let put those behaviors in a proper place.

type Bouncer(name: string) =
    member this.HaveABreak() =
        // details
    member this.StartGuarding() =
        // details
    member this.ReportNewPeople(newPeopleCount: int) =
        // details

I like to focus on behaviors firstly, to see how they drive the inforomation needed.

Clearly, we need some "status" to indicate whether a bouncer is guarding or not.

type Bouncer(name: string) =
    let mutable isGuarding = false // πŸ‘ˆ new!
    member this.HaveABreak() =
        isGuarding <- false // πŸ‘ˆ new!
    member this.StartGuarding() =
        isGuarding <- true // πŸ‘ˆ new!
    member this.ReportNewPeople(newPeopleCount: int) =
        // details

Great. What about ReportNewPeople and information needed?

type Bouncer(name: string) =
    let mutable isGuarding = false    
    let mutable peopleCount = 0 // πŸ‘ˆ new!
    member this.HaveABreak() =
        isGuarding <- false
    member this.StartGuarding() =
        isGuarding <- true
    member this.ReportNewPeople(newPeopleCount: int) =
        // details

Finally, let's apply the first consistency rule.

type Bouncer(name: string) =
    let mutable isGuarding = false 
    let mutable peopleCount = 0   
    member this.HaveABreak() =
        isGuarding <- false
    member this.StartGuarding() =
        isGuarding <- true
    member this.ReportNewPeople(newPeopleCount: int) =
        if isGuarding // πŸ‘ˆ new!
          then peopleCount <- peopleCount + newPeopleCount // πŸ‘ˆ new!
          else failwith $"Bouncer {name} is not guarding at the moment" // πŸ‘ˆ new!

Note that a Bouncer starts with a break!

We got it, isn't it?

But let's come back to our rules. Can a Bouncer satisfy all of them?

βœ… bouncer can be either guarding or having a break βœ… bouncer can report new people only when guarding ❌ when scheduling a break, at least two bouncers needs to be guarding ❌ no more than 100 people partying ❌ at least three bouncers

There are at least three rules we are clearly missing. Let's implement them and see where they fit in.

Rule: when scheduling a break, at least two bouncers needs to be guarding
let atLeastTwoGuarding (bouncers: Bouncer list): bool =
    let guardingBouncersCount =
        bouncers // Bouncer list
        |> List.filter (fun bouncer -> bouncer.IsGuarding) // bool list
        |> List.length // int

    guardingBouncersCount > 2

Easy. NEXT!

Rule: no more than 100 people partying
let reportedLessThan100PeopleWith (newPeopleComing: int) (bouncers: Bouncer list): bool =
    let currentlyPartyingPeople =
        bouncers // Bouncers list
        |> List.map(fun bouncer -> bouncer.PeopleCounted) // int list
        |> List.sum // int
    
    currentlyPartyingPeople + newPeopleComing <= 100

Business rules might be interesting, isn't it? Next please!

We have exposed to the external world some new information, let's add them to the Bouncer.

type Bouncer(name: string) =
    let mutable isGuarding = false 
    let mutable peopleCount = 0
    member this.IsGuarding with get() = isGuarding // πŸ‘ˆ new!
    member this.PeopleCount with get() = peopleCount // πŸ‘ˆ new!
    member this.HaveABreak() =
        isGuarding <- false
    member this.StartGuarding() =
        isGuarding <- false
    member this.ReportNewPeople(newPeopleCount: int) =
        if isGuarding
          then peopleCount <- peopleCount + newPeopleCount
          else failwith $"Bouncer {name} is not guarding at the moment"

Finally, the last missing rule!

Rule: at least three bouncers
let ensureAtLeastThreeBouncers (bouncerNames: string list) =
    let bouncersCount = List.length bouncerNames
    if bouncersCount < 3
        then failwith "At least three bouncers are required!" // β›” consistency violation

No way a single bouncer could satisfy those three rules. We need something "bigger".

Thanks to carefully listening to Tom's explanation, we know that bouncers work together on a shift.

Let's model that and see if this makes any sense!

type BouncersShift(bouncerNames: string list) =
    do ensureAtLeastThreeBouncers bouncerNames // πŸ‘ˆ consistency rule
    let mutable bouncers = bouncerNames |> List.map (fun name -> (name, Bouncer name)) |> dict
      // Dictionary<string, Bouncer>
    member private this.Bouncers with get() = bouncers.Values |> Seq.cast |> List.ofSeq

    member this.ScheduleBreakFor(bouncerName: string) =
        let bouncer = bouncers[bouncerName]
        if atLeastTwoGuarding this.Bouncers // πŸ‘ˆ consistency rule
            then bouncer.HaveABreak()
            else failwith "Can't schedule break because less than two bouncers are guarding" // β›” consistency violation

    member this.StartGuarding(bouncerName: string) =
        let bouncer = bouncers[bouncerName]
        bouncer.Guard()

    member this.ReportNewPeopleComing(bouncerName: string, newPeopleComing: int) =
        let bouncer = bouncers[bouncerName]
        if this.Bouncers |> reportedLessThan100PeopleWith newPeopleComing // πŸ‘ˆ consistency rule
            then bouncer.ReportNewPeople(newPeopleComing)
            else failwith "Can't allow more people enter the club" // β›” consistency violation

In this example we can safely assume that visibility of the Bouncer class is scoped only to BouncersShift class.

Cool, BouncersShift captures the three missing rules, the rest is delegated to a specific Bouncer.

How could we use such a model?

let shift = new BouncersShift(["mike";"john";"derek"])

shift.StartGuarding("mike")
shift.StartGuarding("derek")
shift.StartGuarding("john")
shift.ReportNewPeopleComing("mike", 20)
shift.ScheduleBreakFor("mike")
try shift.ReportNewPeopleComing("mike", 20) with ex -> printfn $"{ex}"
// \__ System.Exception: mike bouncer is not guarding at the moment
shift.ReportNewPeopleComing("derek", 40)
shift.ReportNewPeopleComing("derek", 40)
try shift.ReportNewPeopleComing("derek", 40) with ex -> printfn $"{ex}" 
// \__ System.Exception: Can't allow more people enter the club
try shift.ScheduleBreakFor("derek") with ex -> printfn $"{ex}"
// \__ System.Exception: Can't schedule break because less than two bouncers are guarding
shift.StartGuarding("mike")
shift.ScheduleBreakFor("john")

Whenever there is a consistency violation, our shift shouts with an exception.

How about the rules, is the BouncersShift satisfying all of them?

βœ… bouncer can be either guarding or having a break βœ… bouncer can report new people only when guarding βœ… when scheduling a break, at least two bouncers needs to be guarding βœ… no more than 100 people partying βœ… at least three bouncers

"Whatever happens during the shift, stays in the shift" one might say, but it is quite visible that the shift protects the rules we defined, so the aggregate boundary is suitable for this particular problem.

We found good boundaries!

From now on, we are going to work with both Bouncer and BouncersShift.

Ok, let's move on to the next model.

Solution #2: Functional-oriented approach

This time, we are going to do some lightweight functional acrobatics in order to encode invariants in types.

Type definitions first!

[<RequireQualifiedAccess>]
  module BouncersShift =
      [<RequireQualifiedAccess>]
      module internal Bouncers =
          type GuardingBouncer = private GuardingBouncer of peopleCount: int * name: string
          type BouncerHavingABreak = private BouncerHavingABreak of peopleCount: int * name: string
          type Bouncer = Gaurding of GuardingBouncer | HavingABreak of BouncerHavingABreak

          type private StartGuarding = BouncerHavingABreak -> GuardingBouncer
          type private HaveABreak = GuardingBouncer -> BouncerHavingABreak
          type private ReportNewPeople = int -> GuardingBouncer -> GuardingBouncer

This time we decided to model a bouncer with a two possible states.

We do not have the implementation yet, but we delegated the rules to the compiler.

Let the types guide us.

// still within internal Bouncers module
let startGuarding: StartGuarding = // BouncerHavingABreak -> GuardingBouncer
    fun (BouncerHavingABreak (count, name)) ->
        GuardingBouncer (count, name)

let haveABreak: HaveABreak = // GuardingBouncer -> BouncerHavingABreak
    fun (GuardingBouncer (count, name)) ->
        BouncerHavingABreak (count, name)
        
let reportNewPeople: ReportNewPeople = // int -> GuardingBouncer -> GuardingBouncer
    fun newPeople (GuardingBouncer (count, name)) ->
        GuardingBouncer (count + newPeople, name)

let countPeople (bouncer: Bouncer) = // Bouncer -> int
    match bouncer with
    | Guarding (GuardingBouncer (count, _)) -> count
    | HavingABreak (BouncerHavingABreak (count, _)) -> count

let startShift (name: string) = (BouncerHavingABreak (0, name)) // string -> BouncerHavingABreak

The most intersting part here is reportNewPeople function that works only with GuardingBouncer. Also, we added a helper function countPeople so that we are able to provide this number outside of the module.

Yet again, a Bouncer starts with a break.

Now, a BouncersShift. This time we will try to encode problems within a shift (previously we used exceptions).

T Y P E S first!

// within public BouncersShift module
type BouncersShift = private BouncersShift of (Map<string, Bouncers.Bouncer>)
//            __________ we are representing problems explicitly
//          /
type BouncersShiftProblem = 
    private
    | CountingRefused of name: string * reason: string
    | PartyingPeopleLimitReached
    | SchedulingABreakRejected of reason: string

//                                 ______ we are encoding problems in type signature                           
type StartGuarding =  //         /
    string -> BouncersShift -> Result<BouncersShift, BouncersShiftProblem * BouncersShift>

type ScheduleBreakFor = 
    string -> BouncersShift -> Result<BouncersShift, BouncersShiftProblem * BouncersShift>

type ReportNewPeopleComing = 
    string -> int -> BouncersShift -> Result<BouncersShift, BouncersShiftProblem * BouncersShift>

I hope you are wondering why such a strange result of those three operations? Let's wait to see the example.

To be truly functional (and maybe a bit slow?), we are using Map to represent a bouncers within a given shift. We need to adjust our invariants, but this should be pretty simple.

Rule: when scheduling a break, at least two bouncers needs to be guarding
let private atLeastTwoGuarding (bouncers: Map<string, Bouncers.Bouncer>) =
  let guardingBouncersCount =
    bouncers.Values
    |> (Seq.cast >> List.ofSeq)
    |> List.sumBy (fun bouncer ->
        match bouncer with
        | Bouncers.Guarding _ -> 1
        | _ -> 0
    )

  guardingBouncersCount > 2

I told ya, nothing fancy.

Rule: no more than 100 people partying
let private reportedLessThan100PeopleWith (newPeopleComing: int) (bouncers: Map<string, Bouncers.Bouncer>) =
  let currentlyPartyingPeople = 
          bouncers.Values
          |> (Seq.cast >> List.ofSeq)
          |> List.sumBy Bouncers.countPeople

  currentlyPartyingPeople + newPeopleComing <= 100

Let's assume we're going to leave the last rule with the same implementation. Exception will work as a "fail fast".

let start (bouncerNames: string list) = // string list -> BouncersShift
  ensureAtLeastThreeBouncers bouncerNames
  let bouncers = 
      bouncerNames 
      |> List.map (fun name -> (name, Bouncers.HavingABreak (Bouncers.startShift name)))
      |> Map.ofList
      
  BouncersShift (bouncers)

Neat. We have all building blocks to start implementing three functions: startGuarding, haveABreak and reportNewPeopleComing.

First, let a Bouncer start guarding:

let startGuarding: StartGuarding = 
  // string -> BouncersShift -> Result<BouncersShift, BouncersShiftProblem * BouncersShift>
  fun (name: string) (BouncersShift bouncers) ->
      let bouncer = bouncers |> Map.find name
      match bouncer with
      | Bouncers.HavingABreak bouncerHavingABreak -> 
        let guardingBouncer =
            bouncerHavingABreak 
                |> Bouncers.startGuarding
                |> Bouncers.Guarding
        Ok (BouncersShift (bouncers |> Map.update name guardingBouncer))
      | _ -> Ok (BouncersShift bouncers)

No problems here as a Bouncer can always start guarding. As this is within the BouncersShift module, we can match on the particular state and then wrap the given bouncer again.

This might be a bit tiresome, but hey, we wanted the "type-safety", right?

Next? Scheduling a break for a given Bouncer.

let scheduleBreakFor: ScheduleBreakFor =
  // string -> BouncersShift -> Result<BouncersShift, BouncersShiftProblem * BouncersShift>
  fun (name: string) (BouncersShift (bouncers)) ->
      if atLeastTwoGuarding bouncers // πŸ‘ˆ consistency rule
        then
            let bouncer = bouncers |> Map.find name
            match bouncer with
            | Bouncers.Guarding guardingBouncer -> 
                let bouncerHavingABreak =
                    guardingBouncer
                        |> Bouncers.haveABreak // πŸ‘ˆ consistency rule guided by a compiler
                        |> Bouncers.HavingABreak
                Ok (BouncersShift (bouncers |> Map.update name bouncerHavingABreak))
            | Bouncers.HavingABreak _ -> Ok (BouncersShift bouncers)
        else Error(SchedulingABreakRejected "at least two bounces need to be guarding", BouncersShift bouncers) // β›” consistency violation

As this function is scoped to the BouncersShift module, we are able to reach Bouncer type and match on it.

Ok, without further ado, we are missing the last operation, reportNewPeopleComing, here it comes!

let reportNewPeopleComing: ReportNewPeopleComing =
  // string -> int -> BouncersShift -> Result<BouncersShift, BouncersShiftProblem * BouncersShift>
  fun (name: string) (newPeopleComing: int) (BouncersShift (bouncers): BouncersShift) ->
    if bouncers |> reportedLessThan100PeopleWith newPeopleComing // πŸ‘ˆ consistency rule
      then
          let bouncer = bouncers |> Map.find name
          match bouncer with
          | Bouncers.Guarding guardingBouncer -> 
            let guardingBouncer =
                guardingBouncer 
                    |> Bouncers.reportNewPeople newPeopleComing // πŸ‘ˆ consistency rule guided by a compiler
                    |> Bouncers.Guarding
            Ok (BouncersShift (bouncers |> Map.update name guardingBouncer))
          | Bouncers.HavingABreak _ -> Error(CountingRefused (name, "having a break"), BouncersShift bouncers) // β›” consistency violation
      else Error(PartyingPeopleLimitReached, BouncersShift bouncers) // β›” consistency violation

This is probably the most interesting function so far! Runtime checks didn't disappear, so still we need to match on cases.

But yet again, we are guided by the compiler on "what is possible" within the domain we defined.

What is not possible and should result with a problem, gets encoded in types properly.

As we are dealing with problems explicitly in types, we need to have some helper function:

let handleProblems handleProblem bouncersShift =
// (BouncersShiftProblem -> unit) -> Result<BouncersShift, BouncersShiftProblem * BouncersShift> -> BouncersShift
    match bouncersShift with
    | Ok bouncersShift -> bouncersShift
    | Error (error, bouncersShift) -> 
        handleProblem error
        bouncersShift

//                                       ____ built-in function!
//                                     /
let ignoreProblems = handleProblems ignore

It's not a brave attitude to ignore problems (in one's life), but this time we are going to do so.

Finally, we arrived at the point when we can use our model. Note that the same sequence of operations were used in the OOP solution.

let shift = BouncersShift.start ["mike";"john";"derek"]

shift
|> BouncersShift.startGuarding "mike"
|> (ignoreProblems >> BouncersShift.startGuarding "derek")
|> (ignoreProblems >> BouncersShift.startGuarding "john")
|> (ignoreProblems >> BouncersShift.reportNewPeopleComing "mike" 20)
|> (ignoreProblems >> BouncersShift.scheduleBreakFor "mike")
|> (ignoreProblems >> BouncersShift.reportNewPeopleComing "mike" 20)
//                                        \_______ we could throw an exception here!
|> (ignoreProblems >> BouncersShift.reportNewPeopleComing "derek" 40)
|> (ignoreProblems >> BouncersShift.reportNewPeopleComing "derek" 40)
|> (ignoreProblems >> BouncersShift.reportNewPeopleComing "derek" 40)
//                                        \_______ we could throw an exception here!
|> (ignoreProblems >> BouncersShift.scheduleBreakFor "derek")
//                                        \_______ we could throw an exception here!
|> (ignoreProblems >> BouncersShift.startGuarding "mike")
|> (ignoreProblems >> BouncersShift.scheduleBreakFor "derek")

It might be hard to grasp the output of such pipeline so let me help you with visualizing it:

Ok
    BouncersShift
  (map
     [("derek", HavingABreak (BouncerHavingABreak (80, "derek")));
      ("john", Guarding (GuardingBouncer (0, "john")));
      ("mike", Guarding (GuardingBouncer (20, "mike")))])

As mentioned at the beginning, in this solution we wanted to let our friend, the compiler, help us, mere mortals, leading to the pits of success in "a lightweight fashion".

But wait a minute. As we encoded some possible states of a single Bouncer in types, this means that some states are possible for some operations, whereas other combinations should never happen πŸ€”

I bet there is a good model for tackling such problem in which some transitions between states are possible and others do not affect state changes.

Solution #3: Functional, State Machine-like approach

What would you say if a Bouncer was represented by a state machine?

We've already seen that there are three behaviors of a bouncer: start guarding, have a break and report new people coming.

It turns out those are commands we are sending to the machine. And in the "Solution #2: Functional-oriented approach", we know that there are two possible states: GuardingBouncer and HavingABreakBouncer.

A bouncer as a state machine.

To work with lightweight state machines, we need to define a new module: StateMachines.

module StateMachines =
    type FSM<'Input, 'Output> = FSM of 'Output
    //          \______ used  mostly for documentation - what input is possible
    type Evolve<'Input, 'Output, 'FSM> = 'Input -> 'FSM -> 'Output * 'FSM
    type Aggregate<'Command, 'Event, 'State> = FSM<'Command, 'Event list * 'State>
    //                           'Input   ____________/                \________ 'Output with implicit state

FSM stands for Finite State Machine.

Note that Evolve function and FSM type are handled separately. There is a way to handle them both together, but this will come in the future tale (is it a clickbait? Well, maybe).

What is really interesting, an Aggregate works with commands, events and state. As we implicitly encoded state, when we will evolve our finite state machine, we are going to emit the current state and some facts that happened during the evolution.

As it is still functional approach, let us start with types firstly.

[<RequireQualifiedAccess>]
  module internal Bouncers =
    open StateMachines
    //      \_________ we use types from new module

    type BouncerCommand = private StartGuarding | HaveABreak | ReportNewPeopleComing of count: int

    type BouncerDetails = private { name: string; count: int }

    type BouncerState = Guarding of bouncer: BouncerDetails | HavingABreak of bouncer: BouncerDetails

    //       ______ we are introducing facts happening inside of the state machine
    //      /
    type BouncerEvent =
      | StartedGuarding | StartedABreak | CountingRefused of reason: string | ReportedCount of count: int

    type Bouncer = Bouncer of Aggregate<BouncerCommand, BouncerEvent, BouncerState>
    type private EvolveBouncer = Evolve<BouncerCommand, BouncerEvent list * BouncerState, Bouncer>

So now we defined all our operations as commands. Instead of "problems", that we used in "Solution #2: Functional-oriented approach", we are naming those as "facts" or "events".

We need to bring evolution of a Bouncer state machine into life!

let private NoEvents = List.empty
let private evolve: EvolveBouncer = // BouncerCommand -> Bouncer -> (BouncerEvent list * BouncerState) * Bouncer
    fun (cmd: BouncerCommand) (bouncer: Bouncer) ->
        let (Bouncer(FSM (events, bouncer'))) = bouncer

        let output = // (BouncerEvent list * BouncerState) * Bouncer
            match cmd, bouncer' with
            | StartGuarding, Guarding _ -> NoEvents, bouncer'
            | HaveABreak, Guarding bouncer'' -> [StartedABreak], HavingABreak bouncer''
            | StartGuarding, HavingABreak bouncer'' -> [StartedGuarding], Guarding bouncer''
            | HaveABreak, HavingABreak _ -> NoEvents, bouncer'
            | ReportNewPeopleComing count, Guarding bouncer'' -> // πŸ‘ˆ consistency rule
                [ReportedCount count], Guarding { bouncer'' with count = bouncer''.count + count }
            | ReportNewPeopleComing _, HavingABreak _ -> [CountingRefused "I am having a break"], bouncer' // β›” consistency violation

        let (newEvents, newBouncerState) = output

        output, (Bouncer(FSM(events |> with' newEvents, newBouncerState)))

Everything is in this single function. All possible transitions between the states.

As this function is private, which is a good thing, we need to give some public API.

let startShift name = Bouncer(FSM(NoEvents, HavingABreak { name = name; count = 0}))

let haveABreak = evolve HaveABreak

let startGuarding = evolve StartGuarding

let reportNewPeopleComing count = evolve (ReportNewPeopleComing count)

let countPeople ((Bouncer (FSM((_, bouncer')))): Bouncer) =
    match bouncer' with
    | Guarding guardingBouncerDetails -> guardingBouncerDetails.count
    | HavingABreak havingABreakBouncerDetails -> havingABreakBouncerDetails.count

Yet again, we need to expose countPeople helper function and we already know it will be used by the BouncersShift.

Can we represent a BouncersShift as a state machine too?

Tom clearly stated that we need to maintain the "luxury" level of their clubs. So keeping people outside of the club when limits are reached sounds like a good indicator for having the state when there are no more people allowed and people are allowed to enter.

A shift as a state machine.

You know with what we are going to start firstly, right?

type BouncersShiftCommand = 
  private StartGuarding of name: string | ScheduleBreakFor of name: string | ReportNewPeopleComing of name: string * count: int
type BouncersShiftDetails = private { bouncers: Map<string, Bouncers.Bouncer> }

    //       ______ we are introducing facts happening inside of the state machine
    //      /
type BouncersShiftEvent = 
  | StartedGuarding of name: string | ScheduledBreakFor of name: string
  | CountReported of name: string * count: int | CountingRefused of name: string * reason: string
  | PartyingPeopleLimitReached | SchedulingABreakRejected of reason: string

type BouncersShiftState = 
  private NoMorePeopleAllowed of BouncersShiftDetails | MorePeopleAllowed of BouncersShiftDetails

type BouncersShift = 
  private BouncersShift of Aggregate<BouncersShiftCommand, BouncersShiftEvent, BouncersShiftState>

type private EvolveBouncersShift = 
  Evolve<BouncersShiftCommand, BouncersShiftEvent list * BouncersShiftState, BouncersShift>

We modeled aforementioned NoMorePeopleAllowed and MorePeopleAllowed states explicitly. From the state machine transition diagram we can see that even though the club does not allow for more people, bouncers can go for breaks as usual.

Let's see how could we evolve such machine.

let evolve: EvolveBouncersShift = // BouncersShiftCommand -> BouncersShift -> (BouncersShiftEvent list * BouncersShiftState) * BouncersShift
  fun (cmd: BouncersShiftCommand) (bouncersShift: BouncersShift) ->
    let (BouncersShift(FSM(events, (shiftDetails)))) = bouncersShift
    
    let result = // (BouncersShiftEvent list * BouncersShiftState) * BouncersShift
        match shiftDetails with
        | NoMorePeopleAllowed shiftDetails' ->
            handleShiftWhenNoMorePeopleAllowed cmd shiftDetails'
        | MorePeopleAllowed shiftDetails' ->
            handleShiftWhenMorePeopleAllowed cmd shiftDetails'

    let (newEvents', squadState) = result

    result, BouncersShift(FSM(events |> List.append newEvents', squadState))

It might be too intimidating to show all at once, so let's assume we decided to abstract away some details. As handleShiftWhenMorePeopleAllowed function is much more interesting, because it contains the transition to a "quasi-terminate" state, let's see how it could look like.

let handleShiftWhenMorePeopleAllowed (cmd: BouncersShiftCommand) (shiftDetails': BouncersShiftDetails) = // BouncersShiftCommand -> BouncersShiftDetails -> (BouncersShiftEvent list * BouncersShiftState) * BouncersShift
    match cmd with
    | StartGuarding bouncerName ->
        handleBouncersWhenMorePeopleAllowed (Bouncers.startGuarding) bouncerName shiftDetails'

    | ReportNewPeopleComing (_, newPeopleComingCount) 
        when shiftDetails'.bouncers |> (not << reportedLessThan100PeopleWith newPeopleComingCount) -> // πŸ‘ˆ consistency rule
        [PartyingPeopleLimitReached], NoMorePeopleAllowed shiftDetails' // β›” consistency violation

    | ReportNewPeopleComing (bouncerName, newPeopleComingCount) ->
        handleBouncersWhenMorePeopleAllowed (Bouncers.reportNewPeopleComing newPeopleComingCount) bouncerName shiftDetails'

    | ScheduleBreakFor _
        when (not << atLeastTwoGuarding) shiftDetails'.bouncers -> // πŸ‘ˆ consistency rule
        [SchedulingABreakRejected "Can't schedule break because less than two bouncers are guarding"], MorePeopleAllowed shiftDetails' // β›” consistency violation

    | ScheduleBreakFor bouncerName ->
        handleBouncersWhenMorePeopleAllowed (Bouncers.haveABreak) bouncerName shiftDetails'

Some cases use handleBouncersWhenMorePeopleAllowed function, it's because we wanted to track all facts that happen during the shift.

Now we need to "translate" facts mentioned by each bouncer. Also, this is the place when are are actually delegating the work to a particular bouncer.

let private translateFacts bouncerName (bouncerFacts: Bouncers.BouncerEvent list): BouncersShiftEvent list =
    bouncerFacts
    |> List.map (fun fact ->
        match fact with
        | Bouncers.StartedGuarding -> StartedGuarding bouncerName
        | Bouncers.StartedABreak -> ScheduledBreakFor bouncerName
        | Bouncers.ReportedCount count -> CountReported (bouncerName, count)
        | Bouncers.CountingRefused reason -> CountingRefused (bouncerName, reason)
    )
let private handle cmd bouncerName shift =
    let bouncer = shift.bouncers[bouncerName]
    let ((bouncerFacts, _), bouncer') = bouncer |> cmd
    let bouncers =
        shift.bouncers |> Map.update bouncerName bouncer'
    (translateFacts bouncerName bouncerFacts), { shift with bouncers = bouncers }  

let private handleBouncersWhenMorePeopleAllowed cmd bouncerName shift =
    handle cmd bouncerName shift
    |> fun (events, shiftState) -> events, MorePeopleAllowed shiftState

As with a Bouncer finite state machine, evolve function is a private function which we don't want to show (it is the implenentation details).

So let's bring a public API for a BouncersShift.

let startGuarding name = evolve (StartGuarding name)

let scheduleBreakFor name = evolve (ScheduleBreakFor name)

let reportNewPeopleComing name count = evolve (ReportNewPeopleComing (name, count))

let private toBouncers bouncerNames =
  bouncerNames
      |> List.map (fun name -> name, Bouncers.startShift name)
      |> Map.ofList

let private NoEventsWhenStarting = List.empty
let start bouncerNames =
  ensureAtLeastThreeBouncers bouncerNames
  let bouncers = bouncerNames |> toBouncers
  
  BouncersShift(FSM (NoEventsWhenStarting, MorePeopleAllowed { bouncers = bouncers }))

It's pretty straightforward, isn't it? The interesting part is the verbosity of the last line - we are starting with no events and when more people are allowed.

How about the usage of such shift?

let ignoreFacts = snd // built-in function!
let shift = BouncersShift.start ["mike";"john";"derek"]

shift 
|> BouncersShift.startGuarding "mike"
|> (ignoreFacts >> BouncersShift.startGuarding "derek")
//      \________________ each operation yields both facts that happened
//                        during execution and current state machine
|> (ignoreFacts >> BouncersShift.startGuarding "john")
|> (ignoreFacts >> BouncersShift.reportNewPeopleComing "mike" 20)
|> (ignoreFacts >> BouncersShift.scheduleBreakFor "mike")
|> (ignoreFacts >> BouncersShift.reportNewPeopleComing "mike" 20)
//                    \______ public API haven't changed
|> (ignoreFacts >> BouncersShift.reportNewPeopleComing "derek" 40)
|> (ignoreFacts >> BouncersShift.reportNewPeopleComing "derek" 40)
|> (ignoreFacts >> BouncersShift.reportNewPeopleComing "derek" 40)
|> (ignoreFacts >> BouncersShift.scheduleBreakFor "derek")
|> (ignoreFacts >> BouncersShift.startGuarding "mike")
|> (ignoreFacts >> BouncersShift.scheduleBreakFor "derek")

Yet again, we are totally not interested what happened during the shift. It might be we could utilize such information for some interesting business metric.

In "Solution #2: Functional-oriented approach" example, we haven't tracked anything, since we were dealing with "problems".

Now it is a different story - there are some facts happening and the interpretation of them is on a caller's hands ("the beauty is the observer's eyes").

Nothing is lost, because in this example we are keeping entire history for a particular shift. Let us help ourselves with a simple output visualization.

BouncersShift(FSM
     ([StartedGuarding "mike"; StartedGuarding "derek"; StartedGuarding "john";
       CountReported ("mike", 20); ScheduledBreakFor "mike";
       CountingRefused ("mike", "I am having a break");
       CountReported ("derek", 40); CountReported ("derek", 40);
       PartyingPeopleLimitReached;
       SchedulingABreakRejected "Can't schedule break because less than two bouncers are guarding";
       StartedGuarding "mike"; ScheduledBreakFor "derek"],
      NoMorePeopleAllowed
        { bouncers = map
             [("derek", Bouncer(FSM
                    ([StartedGuarding; ReportedCount 40; ReportedCount 40;
                      StartedABreak], HavingABreak { name = "derek" count = 80 })));
              ("john", Bouncer (FSM ([StartedGuarding], Guarding { name = "john" count = 0 })));
              ("mike", Bouncer (FSM
                    ([StartedGuarding; ReportedCount 20; StartedABreak;
                      CountingRefused "I am having a break"; StartedGuarding],
                     Guarding { name = "mike" count = 20 })))] })))

What's is more, each bouncer also remembers what he did during the shift!

Does it look familiar? How could we utilize such information? More in the next tales! (I know, I know, again clitbait-ish, but I couldn't resist!)

WHAT THE HELL JUST HAPPENED?

It was tough, bumpy, exhaustive, but enjoyable journey (at least for me).

We were working on a single business problem concerned around consistency.

Thanks to the analysis we did, using strategic DDD with language modeling, our problem became "approachable".

We used Aggregate building block from tactical DDD.

We provided three solutions (models) for the problem that Tom challenged us with:

  • Object-oriented approach
  • "Lightweight" Type-driven functional approach
  • "Lightweight" functional, State Machine-like approach

This is also very important - we should always think of multiple models for solving a problem to "promote" the best one (at the current moment). Note that it does not mean all of them need to be perfectly implemented - the main purpose is to experiment and analyze!

Conclusions?

  • OO achieves evolvability through mutability
  • FP achieves evolvability through immutability
  • F# is able to keep implementation details hidden, even though data and behavior are separated
  • There are various ways for handling output from an operation on an aggregate: exception, Result or Event
  • Domain language can be expressed on various levels
  • Type correctness isn't "free"
  • Handling all cases might be tiresome, but might help with crafting. bug-free code

There is also another observation - F# is a great language for domain modeling on variety of levels. Use its powers, let it empower you (I am looking at you F# Compiler!).

Because it is functional-first and not functional-only we use the most suitable approach. I always say that F# made me better Object-oriented programmer.

Ok, we arrived at the place when we can stop. I hope you enjoyed this little exercise my dear Reader.

Thank you for your patience and time!