- Published on
Many faces of DDD Aggregates in F#
- Authors
- Name
- Damian PΕaza
- @raimeyuu
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)!
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:
- Problem?
- DDD 101: strategic and tactical design
- Aggregate?!
- Problem Analysis
- Solution Recommendations (Solution #1 Solution #2 Solution #3)
- Summary
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)

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?

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:
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.
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.

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?

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)

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!
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.
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.
Great. What about ReportNewPeople and information needed?
Finally, let's apply the first consistency rule.
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?
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 guardingEasy. NEXT!
Rule: no more than 100 people partyingBusiness 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.
Finally, the last missing rule!
Rule: at least three bouncersNo 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!
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?
Whenever there is a consistency violation, our shift shouts with an exception.
How about the rules, is the BouncersShift satisfying all of them?
"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!
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.
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!
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.
I told ya, nothing fancy.
Rule: no more than 100 people partyingLet's assume we're going to leave the last rule with the same implementation. Exception will work as a "fail fast".
Neat. We have all building blocks to start implementing three functions: startGuarding, haveABreak and reportNewPeopleComing.
First, let a Bouncer start guarding:
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.
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!
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:
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.
It might be hard to grasp the output of such pipeline so let me help you with visualizing it:
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.

To work with lightweight state machines, we need to define a new module: StateMachines.
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.
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!
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.
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.

You know with what we are going to start firstly, right?
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.
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.
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.
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.
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?
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.
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!