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

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

  1. Use orange sticky notes to create a timeline of domain Events, past-tense verbs that represent important things that happen in the business
  2. Identify triggers
  3. Group related events together to find workflows of closely related events
  4. 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

Timeline
- Site determined distracting/negative
  - Source: User
  - Command: CreateSiteBlock
  - Event: block hours set 
- block hours updated
  - source: user
  - Command: UpdateBlockSchedule
  - REQ: if block running, finish the current block period before applying updates
- ScheduledBlockStarted
  - Source: time / saved schedule
  - Time: `UpdateBlocksBySchedule` triggered by registered schedule
  - note: minimum block increment is 1 minute. Expect blocks to be enforced to the minute.
- ScheduledBlockEnded
  - Source: time / saved schedule
  - `UpdateBlocksBySchedule`
- BlockRulesViewed
  - Command: ListBlockRules
- BlockPaused 
  - Source: user
  - Command: `PauseBlock`
- BlockDeleted
  - source: user
  - REQ: if block running, don't remove until normal period finished or end of day

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

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.

type CreateBlockRule = CreateBlockRuleCommand -> RuleCreated
type UpdateBlockRule = UpdateRuleCommand -> RuleUpdated
type DeleteBlockRule = DeleteBlockRuleCommand -> RuleDeleted
type UpdateRuleActivations = TimeTrigger -> RuleActivationEvent list
type RuleActivationEvent = BlockRuleActivated | BlockRuleDeactivated

type ListBlockRules = unit -> RuleListItemModel list

Refining the design: Values, State Machines, & Constraints

The text representation of events and commands gives us a template to start filling in details like

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

// Creation Input
type CreateBlockRuleCommand = UnvalidatedBlockRule
type UnvalidatedBlockRule = {
    Name: string
    Targets: UnvalidatedBlockTarget list;
    BlockTriggers: UnvalidatedBlockTrigger list
}
type UnvalidatedBlockTarget = Website of string
type UnvalidatedBlockTrigger = Time of (DateTime * DateTime)

// Successful Creation Output
type RuleCreated = RuleCreated of ValidatedBlockRule
type ValidatedBlockRule = {
    Name: RuleName
    Site: ValidatedBlockTarget list;
    BlockTriggers: ValidatedBlockTrigger list
}
type RuleName = RuleName of string
type ValidatedBlockTarget = Site of string
type ValidatedBlockTrigger = TimeTrigger of TimeTrigger
type TimeTrigger =  { Start: Time; End: Time }
//NOTE: we only care about time resolution to the nearest minute. Thus we encode that expectation
type Time = { Hour: int; Minute: int } 

Hmm. A few things to notice

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

type CreateBlockRule = CreateBlockRuleCommand -> Result<RuleCreated, CreateRuleError list>
type UpdateBlockRule = UpdateRuleCommand -> Result<RuleUpdated, UpdateRuleError list>
type DeleteBlockRule = DeleteBlockRuleCommand -> Result<RuleDeleted, DeleteRuleError>
type UpdateRuleActivations = RuleActivationTrigger -> RuleActivationEvent list

type ListBlockRules = unit -> RuleListItemModel list

Then we can identify different possible error cases

type CreateRuleError = 
    | InvalidName of ErrorReason
    | InvalidTarget of (UnvalidatedBlockTarget * ErrorReason) 
    | InvalidTrigger of (UnvalidatedBlockTrigger * ErrorReason) 

type UpdateRuleError = 
    | InvalidName of ErrorReason
    | InvalidTarget of (UnvalidatedBlockTarget * ErrorReason)
    | InvalidTrigger of (UnvalidatedBlockTrigger * ErrorReason) 
    | UnknownRule

type DeleteRuleError = 
    | UnknownRule

type ErrorReason = string

Note that both create and update share most of their failure cases. This hints at a shared sub-flow

type ValidateBlockRule = UnvalidatedBlockRule -> Result<ValidatedBlockRule, RuleValidationError>

type RuleValidationError = 
    | InvalidName of ErrorReason
    | InvalidTarget of (UnvalidatedBlockTarget * ErrorReason) 
    | InvalidTrigger of (UnvalidatedBlockTrigger * ErrorReason) 

type CreateRuleError = 
    | ValidationError of RuleValidationError

type UpdateRuleError = 
    | ValidationError of RuleValidationError
    | UnknownRule

We probably also want to iron out the rule validation constraints with domain experts.

These constraints can also be represented

  let validateHour hour = 0 <= hour && hour <= 23
  let validateMinute minute = 0 <= minute && minute <= 59 
  //...

We can also dig into the dependencies of different operations, like validating a block rule


type ValidateBlockRule = ValidateTrigger -> ValidateName -> ValidateTarget
    UnvalidatedBlockRule -> Result<ValidatedBlockRule, RuleValidationError>

type ValidateTrigger = ValidateTimeTrigger -> Result<BlockTrigger, TriggerValidationError>
type ValidateTimeTrigger = ValidateTime -> Result<BlockTrigger, TimeTriggerValidationError>
//...

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

type UpdateBlockRule = UpdateRuleCommand -> Result<RuleUpdated, UpdateRuleError list>

Let’s improve it.

What should happen if a rule is currently active and a rule is updated.

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.

type UpdateBlockRule = UpdateRuleCommand -> Result<RuleUpdatedEvents, UpdateRuleError list>

type PendingRuleUpdate = { Current: ValidatedBlockRule; Pending of ValidatedBlockRule}
type RuleUpdatedEvents = 
    | RuleUpdated of ValidatedBlockRule
    | UpdatePending of PendingRuleUpdate

This effects more parts of the design though. Our activation updater now needs to deal with multiple states.

type StatefulBlockRule = 
  | Active of ValidatedBlockRule
  | Inactive of ValidatedBlockRule
  | PendingUpdate of PendingRuleUpdate

type RuleActivationEvent = 
  | Activated of BlockRuleActivated 
  | Deactivated of BlockRuleDeactivated
  | UpdateApplied of RuleUpdated

type ListBlockRules = unit -> StatefulBlockRule list 

type UpdateRuleActivations = ListBlockRules ->
 RuleActivationTrigger -> RuleActivationEvent list

The matrix of input states and output states defines a state machine of possible transitions

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.

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.