09 · Supple design
When you finish this lesson you should be able to:
- Explain why a deep model without a supple design still stalls the project
- Apply the chapter 10 toolkit: intention, side-effect-free functions, assertions, contours, standalone classes, closure, declarative style
- Diagnose duplication that comes from fear (“I cannot predict this computation”)
Why “supple”
Section titled “Why “supple””The software serves users, but first it serves developers who will rearrange every part. Without a clean design:
- people will not predict implications, so they duplicate
- monoliths cannot recombine
- tiny pieces cannot be remembered
- everyone dreads looking at the mess
That dread caps how rich the behavior can get. Refactoring stops. You have a shallow ceiling even if the business is deep.
Supple design is the complement of deep modeling. Implicit concepts (lesson 08) are raw material. You hammer them into a shape client developers can actually use. Code leads to insight, insight leads to code. Round and round.
Intention-revealing interfaces
Section titled “Intention-revealing interfaces”The name is the contract. A developer should guess the result without opening the body.
assertCanSelectPlannotcheckassignmentFornotprocessinHoldoutnoteval
If you need a comment to explain the method, the name lost. Experts should be able to hear the name in a walkthrough.
Side-effect-free functions
Section titled “Side-effect-free functions”Calculations and queries should not mutate. quietHoursAllows(hours, at) returns a boolean. Commands that change the aggregate are few and obvious: thread.append.
Benefits: tests without fixtures from hell; composition; less fear.
Put as much of the domain as you can in functions over values. Keep mutation at the aggregate root.
Assertions
Section titled “Assertions”Commands state preconditions and outcomes. append asserts participant and not archived. Fail in the domain, not as a null in the UI tomorrow.
Assertions are how invariants (lesson 06) stay visible. A private if three levels down is not an assertion; it is a landmine.
Conceptual contours
Section titled “Conceptual contours”Split where meaning changes. QuietHours is a contour. “Parse HH:mm” is not a domain object unless time-of-day math is the business.
Over-splitting produces the “too many little parts” failure. Under-splitting produces the monolith. Contours are linguistic: if experts discuss it as a thing, it is a candidate; if only programmers do, it might be mechanism (lesson 13).
Standalone classes
Section titled “Standalone classes”A type you cannot understand without half the system is not standalone. Values should be explainable on a sticky note. If Assignment needs six services to exist, it is a script in a class costume.
Closure of operations
Section titled “Closure of operations”When inputs and outputs are the same type, operations compose: Money.plus(Money), MuteWindow.extend(MuteWindow). You get algebra instead of helperCombineMute(a, b, flags).
Not everything closes. Do not fake it. Where it does (durations, money, specs), lean in.
Declarative style
Section titled “Declarative style”“Eligible if IsEligibleForPlan is satisfied” vs a 40-line nested if in a handler. Specifications and policies describe. You still implement them. The client of the domain reads as speech.
Angles of attack (when the design is stiff)
Section titled “Angles of attack (when the design is stiff)”Extract a value. Extract a spec. Make a function pure. Shrink an aggregate. Rename until a meeting could use the name. Delete a shallow wrapper.
Do not “microservices the mess.” That copies the tangle over the network.
Example
Section titled “Example”Stiff handler:
function handle(req) { if (user.tz && (hour >= 21 || hour < 8)) return; if (user.flags && user.flags.holdout) return; send(req);}Supple:
function mayNotify(policy, userContext, at) { return quietHoursAllows(policy.quietHours, at) && !inHoldout(policy.holdout, userContext);}send is a command on Delivery, elsewhere. mayNotify is closed over values, pure, intention-revealing.
Drill (20 minutes)
Section titled “Drill (20 minutes)”- Three more predicates for
mayNotify(consent, channel, duplicate). - Each is a named function of values.
- One assertion on the send command.
- One predicate that does not belong (it is another contour / aggregate). Move it.
- Read the names out loud. If you would not say them to an expert, rename.
Further reading
Section titled “Further reading”- Domain-Driven Design — Chapter 10, Supple Design