I just finished reading Domain Modeling Made Functional. Wow, the technique in this book just feels right. It wraps up a bunch of development values I already had in a technique that is clear, thorough, and fluid. Here I’ll overview the method and recount a bit of my experience applying it.
I’m a software design nut. I love trying different mental models for approaching problem solving. Here are some of my core values that also motivate this approach
- Understand a problem before coding
- Design to the problem and not the implementation
- Design is a spectrum: progressively uncover knowledge of the problem
- Coding is just the last mile design phase
- Self-documenting code
- Core code should be infrastructure-agnostic
- Orient the api around verbs, not nouns
You can probably tell that this technique starts with problem (domain) modeling. So, we will too. First we’ll talk about understanding a high-level problem with domain experts, then encoding that knowledge, filling in more detail, and lastly translating it to code.
Event Storming
Event storming is a technique that quickly rooted itself in the DDD community, and the first step of this modeling process.
Event storming is pretty straightforward. Get experts from different parts of the company in a room with a bunch of sticky notes. Then
- Use orange sticky notes to create a timeline of domain Events, past-tense verbs that represent important things that happen in the business
- Identify triggers
- use blue stickies for Commands, imperatively phrased words for user actions that trigger events
- use purple stickies for triggers based on time or external systems
- Group related events together to find workflows of closely related events
- Identify sub-domains or places where workflows need to be independent. Sub-domains are often along department boundaries
Event storming is meant to ease communication between many people. However, I was working on this alone. Instead of stickies I just typed it all out in a text document. Here’s a simple sample
|
|
Storm to Design
The event storm outputs a collective understanding of the business process at a high level. The next step is to start working out details of each part with the experts who own it.
This can be done with just talking and note-taking. However, it’s much more powerful to have some shared representation that both parties understand and can comment on.
Some might use UML or diagrams, but let’s take a simple text-based approach.
A few quick rules for understanding the text model
type
starts a definition|
represents alternative values{}
are bundled values->
is a transform from data on the left to data on the right
The workflows from before become transforms, which take the related command as input and return the related events. This makes for an easy translation of the event storm into a format where we can start filling in details.
Here’s the high-level translation.
|
|
Refining the design: Values, State Machines, & Constraints
The text representation of events and commands gives us a template to start filling in details like
- what information do they need to execute a command (still thinking in terms of manual process)
- what different routes can a task take and why
- how do the experts break down their work into smaller steps
- What can go wrong and how is it handled
Some of this might happen in the original event storm. In fact, some of it should if the decisions effect multiple parties.
Values
Here are some of the commands and events filled in
|
|
Hmm. A few things to notice
- We don’t trust the block rule input. Thus it needs to get validated somehow
- A TimeTrigger is just one kind of block trigger. It is the only one for now, but we expect more options will be added. For example, location or user-defined “mode” would be reasonable triggers.
- Similarly, a Website is just one kind of block target. Programs, files, or folders are also reasonable targets to block. They all fit the idea of limiting access to distractions
Errors and Constraints
Let’s dig deeper into the validation. First, our workflows should make it clear when errors are possible. So let’s update them
|
|
Then we can identify different possible error cases
|
|
Note that both create and update share most of their failure cases. This hints at a shared sub-flow
|
|
We probably also want to iron out the rule validation constraints with domain experts.
- Times are represented as 24-hr time. Hours 0-23 are valid and minutes 0-59
- Websites are expected to be domain names, with no sub-path
- Rule Name should not contain tabs or newlines
These constraints can also be represented
|
|
We can also dig into the dependencies of different operations, like validating a block rule
|
|
Each of these definitions uncovers more expectations of the domain clearly without forcing us into an implementation or prototype.
States
Let’s think about the rule update workflow. Our notes from the event storm indicate that a block rule should not be updated or deleted while the rule is in effect. This is to prevent users from circumventing rules.
However, model does not currently indicate this rule
|
|
Let’s improve it.
What should happen if a rule is currently active and a rule is updated.
- We could error and have the user try again later
- We could save the update for after the rule is inactivated
- If the rule is always active?
Our “domain expert” (me) says we want to apply the rule later. If the rule is always active, apply changes at the end of the day.
This means we really have two success criteria.
|
|
This effects more parts of the design though. Our activation updater now needs to deal with multiple states.
|
|
The matrix of input states and output states defines a state machine of possible transitions
- Inactive, no update -> Still inactive
- Inactive and no update -> Becomes active
- Active and no update -> Stays active
- Active and update -> Stays active and holds update
- Active and update -> becomes inactive and applies update
This makes a good talking structure about stateful behavior with business experts.
Design To Code
Now here’s the best part. All those examples above are valid F# code.
Using events, transforms, and state as the basis of our design captures domain requirements in a way that both parties can understand right up to the point where it becomes the high-level code design.
This approach also isn’t limited to functional languages. Changes in C# 9 make many of these techniques quite pleasant.
- For representing alternative values, see my post on union types in c#
- These enable reasonable state machines and compositional polymorphism (wrap rather than inherit)
- See my post on result types in c#. A big step toward total functions
- Positional records can be used to succinctly name and constrain simple types.
My full experiment source is available on github.
Personal Take
This technique is amazing. I’ve never used a modeling paradigm that encoded the domain with such ease and clarity. So much domain information can be encoded just in type signatures that many more issues can be detected before coding ever starts, and the gap to code is so much smaller once coding actually starts. Most domain rules are enforced by the type signatures, which reduces the errors that developers can make from misunderstanding or accident.
I think much of the power comes from focusing on events, transforms, and states rather than data, services, or other programming-focused constructs.
The approach definitely takes some adjustment to how you think, but in a very good way. It disentangles bad habits accumulated from accommodating limited type systems, storage, and the like.
Summary
This event, transform, and state oriented design is an all around winner. It is easier to model the domain, more information is uncovered during modeling, and the resulting code is both safer and easier to write.
I highly recommend Domain Modeling Made Functional for further exploration. It describes the process accessibly and with lots of concrete examples.