Skip to content

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”)

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.

The name is the contract. A developer should guess the result without opening the body.

  • assertCanSelectPlan not check
  • assignmentFor not process
  • inHoldout not eval

If you need a comment to explain the method, the name lost. Experts should be able to hear the name in a walkthrough.

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.

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.

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

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.

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.

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

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.

  1. Three more predicates for mayNotify (consent, channel, duplicate).
  2. Each is a named function of values.
  3. One assertion on the send command.
  4. One predicate that does not belong (it is another contour / aggregate). Move it.
  5. Read the names out loud. If you would not say them to an expert, rename.
  • Domain-Driven Design — Chapter 10, Supple Design