FoundationsGENERALLIFETIME-SPECIFIC

The Design Loop

Requirement, constraints, invariants, responsibilities, boundaries, interfaces, state, dependencies, failure, implementation, tests, feedback, evolution — in that order, because each answer constrains the next.

The requirement, the obvious build, and why it breaks

Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind survives until the requirement changes.

The question

What is the general shape of a design decision, so I can ask the same questions of any feature?

The requirement

Someone asks for a feature. Before any code is written, there is a set of questions whose answers determine everything that follows.

The obvious build

Start coding. The design emerges from the code, and thinking up front is waterfall.

Why it breaks

Design does emerge — but the parts that emerge last are the parts that are most expensive to change: state ownership, boundaries, and the shape of the interface everyone has already called.

How it breaks as requirements change
  • Design does emerge — but the parts that emerge last are the parts that are most expensive to change: state ownership, boundaries, and the shape of the interface everyone has already called.
  • By the time the code exists, several decisions have been made implicitly and are now defended by the code that depends on them.
  • The failure is specific and recognisable: the happy path is written first, and error handling, concurrency and migration get retrofitted into a structure that had no room for them (Designing the Happy Path Last).
  • It also makes review useless. A reviewer looking at a finished implementation can catch bugs but cannot realistically ask for a different boundary (Review as Design Feedback — and Why It Arrives Too Late).
RequirementConstraintsInvariantsResponsibilitiesBoundariesInterfacesStateDependenciesFailureImplementationTestsFeedbackEvolution

What limits the solution, and what must never stop being true

This domain leads with these two. A design that ignores its constraints is not a design, and an invariant nobody named is one nothing is protecting.

Constraints
  • The loop has to be cheap enough to run for a small feature, or it will only ever be run for large ones — where it is least likely to help, because large features are already reviewed.
Invariants
  • The order matters: an answer taken out of order is a guess. You cannot choose a boundary before you know the invariant it protects.

Who owns what, and where the seams fall

Responsibilities decide boundaries; boundaries decide what an interface has to say.

Responsibilities
  • The loop itself owns nothing; it is a checklist. Its value is that the omissions become visible, and omissions are what reviews miss.
  • Each step names a decision someone has to own, so the loop also allocates responsibility.
Boundaries
  • Boundaries sit in the middle of the loop, and that placement is the argument: they come after invariants and responsibilities because those decide where a boundary should fall, and before interfaces because a boundary decides what an interface must expose (Architecture Boundaries).

Thirteen steps, and the question each one asks

Read this as a set of prompts rather than a process. The useful moment is the one where you cannot answer, because that is a decision you were about to make by accident.

The loop, and how each step fails when skipped
  1. 1
    Requirement

    What does someone actually want, in their words?

    fails by Building the mechanism the ticket described rather than the outcome someone needed.

  2. 2
    Constraints

    What limits the solution — team, deadline, existing system, compliance?

    fails by A design that would be right in a different company (Constraints Are Part of the Design).

  3. 3
    Invariants

    What must never stop being true?

    fails by The rule ends up enforced in three places and guaranteed in none (Where Invariants Live).

  4. 4
    Responsibilities

    Who owns each piece of this?

    fails by A class that accumulates every reason to change (God Object).

  5. 5
    Boundaries

    Where should the seam fall so a likely change is contained?

    fails by Boundaries drawn by technical type, so every feature crosses all of them (Package by Layer).

  6. 6
    Interfaces

    What must a caller know, and what can we keep hidden?

    fails by An interface shaped by the implementation, which then cannot change (Designing a Module Interface).

  7. 7
    State

    What state exists, who may mutate it, and what are its legal transitions?

    fails by Four booleans encoding six impossible combinations (Boolean Flag Explosion).

  8. 8
    Dependencies

    What does this depend on, and is the direction right?

    fails by Policy depending on a volatile detail (Dependency Direction).

  9. 9
    Failure modes

    What happens when the dependency is slow, absent, or answers twice?

    fails by Error handling retrofitted into a structure with no room for it (Failure-Aware Feature Design).

  10. 10
    Implementation

    Write the smallest thing that satisfies the above.

    fails by Being the first step rather than the tenth.

  11. 11
    Tests

    What is tested, at which boundary, and why that one?

    fails by Tests that mirror the implementation and block every refactor (Mocking).

  12. 12
    Feedback

    How will we know in production whether this works?

    fails by A feature that can only be debugged by reading the code (Debuggability by Design).

  13. 13
    Evolution

    What will the next change to this cost?

    fails by Never being asked, which is how change amplification accumulates unnoticed.

The order is not arbitrary. Invariants decide boundaries; boundaries decide interfaces; interfaces decide what can be tested in isolation. Answering out of order means guessing.

Where the loop actually earns its keep

Three steps repay the whole exercise, and all three are ones an implementation-first approach reaches last: invariants, failure modes and evolution. They share a property — each is cheap to decide now and expensive to retrofit, because each shapes the structure rather than sitting inside it.

The others are useful mostly because they force these three to be reachable.

responsibilitiesOrderService, as it exists in most codebases after two yearsThe loop applied to one unit
Knows
  • How a price is calculated
  • Which table orders live in
  • The email template for confirmations
  • Which payment provider is configured
  • What an order status string means
Does
  • Validates input
  • Computes totals
  • Writes to the database
  • Calls the payment provider
  • Sends email
  • Writes an audit record
Depends on
  • Database
  • Payment SDK
  • SMTP client
  • Template engine
  • Clock
  • Config
Changes when — 7 distinct reasons
  • Pricing rules change
  • The schema changes
  • The email copy changes
  • The payment provider changes
  • A new order status is added
  • The audit format changes
  • Anything about validation changes

Seven distinct reasons to change is the finding. Any one of them forces a redeploy and a full regression of the other six, and every one of those seven teams now queues behind the same file. The fix is not "split it into seven classes" — it is to notice that pricing, persistence and notification are three different rates of change, and to give the two most volatile ones their own address first.

How to build it

Most important first.

  • Run it in order and out loud. Most of the value is in noticing which step you cannot answer.
  • Timebox it to minutes for a small feature. A loop that takes an afternoon will be skipped, which is worse than a rough answer to each step.
  • Write down the answers where the code lives, so the reasoning survives the author (Docs Close to Code).
  • Treat "I do not know" as an answer that stops implementation rather than one you code past.

What the next change costs

The field this whole domain exists for. A structure is only better if it makes the change after this one cheaper — and it is worth saying which changes it does not help.

Cost of the next change
  • Running the loop costs minutes now against the possibility of a boundary you cannot move later. Its whole return is in the steps you would otherwise have answered implicitly.
  • The steps that pay for it most reliably are invariants, failure modes and migration, because those are the three that are most expensive to retrofit and most likely to be skipped.
What the recommended approach costs
  • Any checklist trades some autonomy for consistency, and experienced engineers legitimately resent that. The counter is that the loop is a prompt, not a gate.
  • It biases toward deliberation, which is wrong for genuinely trivial changes and for prototypes that will be deleted.

What can go wrong

Failure modes
  • The loop becomes a template that is filled in after the fact to satisfy a process, which produces documents and no thinking.
  • It is run only for large features, so the small changes that quietly accumulate design debt are never examined.
  • It is treated as a sequence to complete rather than as a set of questions to fail on — the point is to get stuck.
Dependencies, and their direction
  • Each step depends on the ones before it. Choosing an interface before knowing the state it manages produces an interface that leaks the implementation (Designing a Module Interface).
Misreads
  • "This is big design up front." It is thirteen questions, most answerable in a sentence. Big design up front is committing to an architecture before building anything; this is refusing to choose a boundary by accident.
  • "Emergent design means no thinking." Emergent design means deferring decisions until you have evidence — which requires knowing which decisions you are deferring.
  • "We do this in code review." Review happens after the boundary exists, and moving a boundary at review time is expensive enough that it almost never happens.

Testing it, and how it ages

What to test, and at which boundary
  • The tests step is inside the loop deliberately: deciding what to test at which boundary is a design decision, and deferring it until after implementation means the tests describe whatever got built (Testing as Design Feedback).
How this design ages
  • The loop's last step is evolution, which feeds the next requirement back to the first step. That is the shape of a codebase that stays workable: every change re-enters at the top.

Where this applies

This domain's advice is contested more than most. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view rather than a caricature.

  • GENERALThe dependency order between the steps follows from what each decision needs as input, so it holds regardless of language, paradigm or process.
  • LIFETIME-SPECIFICFor a spike or a prototype being deleted next week, most of the loop is waste and the honest answer is to skip it — but that decision should be made explicitly, because prototypes that ship are how most of this domain's problems begin.

Where the depth lives

This domain teaches the codebase-level structure and hands the rest off.

Domains that do not exist yet
  • System Design — the same loop runs at a larger grain, where "boundaries" means services and "failure modes" means partitions rather than exceptions.