FeaturesGENERALSCALE-SPECIFICCONTESTED

A Feature Design Template

Twelve fields, filled in for a real feature. The value is not the document — it is that a blank field is visible in a way an unasked question is not.

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

How do I make the questions I skipped visible to a reviewer, without turning feature work into a documentation process?

The requirement

The pause feature shipped and immediately produced two incidents nobody had considered. The team wants a way to catch that class of omission at review time, on a page, in fifteen minutes.

The obvious build

Write a design doc. A page of prose covering what we are building and why, reviewed before implementation. It is what every senior engineer has been taught to produce, and prose is genuinely better at explaining a decision than a form is.

Why it breaks

Prose has no blanks. A page that never mentions migration reads as complete, because the reader has no way to distinguish "we decided no migration is needed" from "nobody thought about migration" (The Design Loop).

How it breaks as requirements change
  • Prose has no blanks. A page that never mentions migration reads as complete, because the reader has no way to distinguish "we decided no migration is needed" from "nobody thought about migration" (The Design Loop).
  • Prose expands to fill the time available. The doc for a two-day feature becomes three pages, so it is written for large features only — which are the ones already getting reviewed.
  • It ages into a lie faster than code, because nothing forces it to change when the code does, and a wrong design doc is more expensive than none (Documentation Decay).
  • The second reviewer reads a different doc from the first, because prose has no fixed shape, so the review is not repeatable across people or features.
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
  • It has to fit on one screen or it will not be filled in, and a template nobody fills in is worse than none because it manufactures the appearance of rigour.
  • It has to live next to the code, because a wiki page diverges from the implementation within one sprint (Docs Close to Code).
  • It cannot become a gate. The moment it blocks a merge, it gets filled in retrospectively with whatever the code already does.
  • The team is five engineers with no dedicated reviewer, so the reader is a peer with twenty minutes, not an architect with an afternoon.
Invariants
  • A field left blank stays visibly blank. The template's entire mechanism is that absence is rendered, and any convention that lets a field be quietly deleted destroys it.
  • The filled template describes intent before implementation. If it is written after the code, it is a summary and it catches nothing.

Who owns what, and where the seams fall

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

Responsibilities
  • The template owns the shape of the question set — which fields exist and in which order. It owns no answers.
  • The author owns each answer and, crucially, owns writing "N/A, because …" rather than deleting a field. The reason is the artifact.
  • The reviewer owns interrogating the blanks first and the prose second, which is the opposite of how design docs are usually read.
  • Nobody owns keeping it current after the feature ships — it is a record of a decision at a moment, not documentation, and pretending otherwise is what makes teams abandon them (Architecture Decision Records).
Boundaries
  • The template stops at the feature boundary. Anything that spans features — a storage choice, a service split — is an architecture decision and belongs in a decision record with its own lifecycle (Decision Records).
  • It lives in the repository, next to the module it describes, so the diff that changes the behaviour can change the design in the same review.
  • It stops before implementation detail. "We will use a state machine" belongs in it; "the enum lives in subscription/state.ts" does not, because that is what the code says.

The twelve fields

The order matters for the same reason it matters in The Design Loop: Invariants cannot be answered before Inputs and Outputs are known, and State cannot be answered before Invariants. Reading it out of order produces answers that quietly contradict each other.

The last four — Security, Observability, Migration, Tests — are last because they are the ones that get skipped, and putting them last means their blankness is the final thing a reviewer sees. That is a deliberate piece of layout, not an ordering by importance.

design.md, next to the module it describes
1# <feature>
2
3Problem What someone asked for, in their words. Not the solution.
4Actors Who or what initiates this, and with what authority.
5Inputs What comes in, from where, and how far it is trusted.
6Outputs What changes in the world, and who observes it.
7Invariants What must never stop being true. The bug list, pre-written.
8State Which states exist, which transitions, which are forbidden.
9Dependencies What this calls, in which direction, and what if it is gone.
10Failure Modes DB down, dependency slow, request repeated, half-done.
11Security Who may do this, whose data is it, what must not be logged.
12Observability The question support asks in six weeks, and what answers it.
13Migration What happens to rows that already exist, and mid-deploy.
14Tests What is asserted, at which boundary, and what is not.
15
16# Any field may be "N/A, because ..." — no field may be deleted.

The last line is the whole mechanism. Deletion makes an omission invisible; "N/A, because the feature adds no state" is a claim, and a claim can be wrong in review — which is the only way this catches anything (Review as Design Feedback — and Why It Arrives Too Late).

Filled in: Pause Subscription

DOMAIN-SPECIFICA billing lifecycle makes State and Invariants the heavy fields and Inputs nearly trivial. Invert that for an ingestion feature, where Inputs and their trust level carry the whole design and the state model is one enum with two values — the field set is the same, the weight distribution is not.

Written before implementation, in about twenty minutes. Two answers here are wrong in ways the team only discovered later, and they are left as they were — a template that only ever shows correct answers teaches the wrong lesson about what it is for.

The Failure Modes and Migration lines are the ones that changed the code. Everything above them mostly confirmed what the team already intended; those two produced an idempotency key and a two-phase migration that nobody had planned on Monday.

# Pause Subscription

Problem        Customers who want to stop paying for a few months cancel
               instead, and mostly do not come back. They want to keep
               their history and their seat.
Actors         The account owner (self-service) and support staff acting
               on their behalf. Not billing admins - decided, not assumed.
Inputs         subscription_id, pause_until (optional), reason (optional),
               Idempotency-Key header. All from an authenticated session.
Outputs        Subscription state -> paused; a PauseStarted event; an
               audit row; a confirmation email; finance sees it next export.
Invariants     A paused subscription is never charged. Exactly one state at
               a time. Every transition is attributable to an actor.
State          active -> paused (guard: not mid-charge, <= 3 months/year)
               paused -> active (resume, manual or scheduled)
               paused -> cancelled (allowed)
               FORBIDDEN: cancelled -> paused; paused -> paused.
Dependencies   Billing job reads the lifecycle (inward). Entitlement reads
               state (inward). Email is fire-and-forget (outward, may fail).
               Clock is injected. No new external services.
Failure Modes  Nightly job selected its batch before the pause committed
               -> charge happens anyway. Retry doubles the pause window.
               Email sends, DB write rolls back. Card expires while paused.
Security       Owner or support only; support actions require a reason and
               are audited. Reason text is user input - never in logs, it
               will contain "moving house, hospital, divorce".
Observability  "Why was this customer charged on the 3rd?" -> subscription
               transition log keyed by subscription_id, plus the billing
               job's decision line naming the state it saw.
Migration      Add state column, backfill from the paused boolean, write
               both for one release, then stop reading the boolean, then
               drop it. Three deploys, each independently revertible.
Tests          Transition table incl. all forbidden pairs (unit). Billing
               job does not charge a paused subscription (integration).
               Double-pause with one key = one pause (contract).
               NOT tested: that the handler calls the repository.

What each blank would have cost

This is the argument for the field set, and it is retrospective on purpose: every row below is a real class of incident, and each one is what a specific empty line looks like six weeks later.

Two of these hit the team that shipped pause. The idempotency one was caught by the template and cost an hour; the entitlement one was not on the form at all, which is the honest limit of the technique.

Field left blank, and where it surfaces
TriggerSymptomCauseResponse
Invariants blankA paused customer is charged during a partial outage, and nobody can say whether that is a bug or an accepted edge case.The rule was never stated, so no test asserts it and no reviewer could have missed it.One sentence in the field; one integration test that quotes it verbatim (Enforcing Invariants).
State blankSix months on, four booleans encode a lifecycle and three combinations are unreachable by accident rather than by design.Each flag was added for one requirement without anyone owning the state space as a whole.Name the states before the first flag exists (Making Illegal States Unrepresentable).
Failure Modes blankA retried pause request doubles the pause window; support fixes it by hand for eleven customers.The happy path was designed and the repeat-request case was never a case at all (Designing the Happy Path Last).Idempotency key on the command, decided before the handler is written (Idempotency by Design).
Security blankPause reasons — "hospital", "divorce" — end up in application logs and then in the log vendor, out of the deletion path.Nobody asked which inputs were sensitive, because the feature obviously was not a security feature.Mark sensitive fields at the type level so logging them is a compile error, not a review catch (Sensitive State).
Observability blank"Why was this charged on the 3rd" takes two engineers a day, and the answer is a guess because the state has since changed.Only current state was stored; the decision that produced the charge left no trace.Log the transition and the job's decision, keyed by subscription id (Stable Identifiers).
Migration blankThe deploy needs code and schema to land atomically. They do not, and for four minutes both readers are wrong.The migration was treated as a step in the deploy rather than as a design decision with its own states.Expand, migrate, contract — three deploys, each revertible on its own (Expand and Contract).

How to build it

Most important first.

  • Twelve fields, fixed order, one screen. Fixed because a template whose fields vary per feature cannot make an omission visible — the reader has to already know which field is missing.
  • Require "N/A, because …" rather than deletion. Nine tenths of the value is in the sentences that begin that way, since they convert a silent skip into a claim someone can disagree with.
  • Put worked examples in the template itself. An empty field prompts a paragraph; a field showing what a good answer looked like last time prompts a comparable one.
  • Fill it in the order given, and stop when you cannot answer. The stall is the finding, and it is cheaper here than in the third week of implementation.
  • Review the blanks before the prose. A reviewer who reads top to bottom is being led by what the author chose to explain, which is precisely the failure mode of prose docs (A Review Checklist Worth Reading).
  • Delete the template for features that do not need it, out loud, in the pull request. "No design doc: this is a copy change" is a design statement and takes eight seconds.

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
  • Adding a thirteenth field costs one line in the template and a slow decline in completion rate for every field. Field count is the design decision here, and it is one-way in practice.
  • Changing an answer after the code exists costs one edit and one review comment, because the record sits in the same repository as the code — this is the entire argument for it not living in a wiki.
  • The next feature costs about fifteen minutes of filling in, and roughly zero discovery, because the questions are already listed. That is the compounding return: the second use is much cheaper than the first.
  • Abandoning the template later costs nothing, which is the property that makes it safe to try. Very little else in this domain is that reversible (Reversible and Irreversible Decisions).
What the recommended approach costs
  • Any template trades thinking for recall. It reliably catches the question you forgot and reliably fails to prompt the question nobody has ever asked, and the second class is where the interesting incidents come from.
  • Fixed fields impose a shape on features that do not have it. A pure algorithm change has no Actors and no Migration, and forcing three "N/A" lines is friction that buys nothing that day.
  • Writing it before implementation means committing to answers you will revise. Some will be wrong, and a record of a wrong decision is genuinely worse than no record if anyone treats it as current (Documentation Decay).

What can go wrong

Failure modes
  • It becomes mandatory, so it is completed after implementation from the code. The fields are all full, none of them caught anything, and the team now believes they have design review.
  • Fields get quietly dropped when they are awkward — Migration and Security first, every time — which removes exactly the fields that exist because they are awkward.
  • It grows. Someone adds Rollout, then Cost, then Accessibility, and at twenty fields it exceeds one screen and stops being filled in at all.
  • The mitigation fails too: teams that make it optional often find it is used only by the engineers who would have thought about migration anyway, which is a real result and not a reason to make it mandatory.
Dependencies, and their direction
  • The template depends on the domain vocabulary being stable enough that "invariant" and "actor" mean the same thing to five people. On a new team that vocabulary has to be built first, or the fields get filled with mismatched content (Ubiquitous Language).
  • It depends on review culture. In a team where review is a rubber stamp, the template becomes a form, and forms are filled in to be accepted rather than to be true (Tone, Disagreement and Receiving Review).
  • Nothing depends on it in code, deliberately. A template that generates scaffolding becomes a framework, and then it cannot be skipped (What a Framework Charges).
Misreads
  • "So we need a design doc for everything." The opposite: the template exists so that most features need fifteen minutes rather than a document, and so that skipping it is a decision rather than a habit.
  • "The template is the design." The template is where the design is recorded. A full template describing a bad state model is a well-documented bad state model.
  • "Blank fields mean the author was careless." Sometimes a blank field is the correct answer and the author should write why. The failure is the field that was deleted, not the one left empty.
  • "This is the same as an ADR." Different lifetime and different scope. An ADR records a decision meant to outlive the feature and gets superseded rather than edited; this is a snapshot of one feature's reasoning (Architecture Decision Records).

Testing it, and how it ages

What to test, and at which boundary
  • The template is not code and has no tests. It has a check, and the check is: take the last two incidents, and ask whether any field would have caught them. If the answer is no twice running, the fields are wrong.
  • Test the claims the template makes, not the template. "Invariants: a paused subscription is never charged" becomes an integration test, and the link between the two is what makes the field more than a wish (Where a Test Must Be Real).
  • A useful audit: pick a completed template at random and diff its Failure Modes against the code. Divergence tells you whether the record described intent or transcribed implementation.
How this design ages
  • The field set drifts toward what the team keeps getting wrong. A team burned twice by migrations grows a sharper Migration prompt, and that specialisation is the template working, not scope creep.
  • It stops being useful when everyone has internalised the questions — at which point the fields get answered in the pull request description in three lines, which is the intended terminal state rather than a failure.
  • It has to change when the system does. A template written for a monolith has one Dependencies field; the same team on four services needs it to distinguish in-process from network calls, because those fail differently (What Changes at the Network Boundary).

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.

  • GENERALThat a rendered blank is more visible than an unasked question is a property of forms rather than of software, so the mechanism carries across languages and team structures. What varies is which twelve fields matter, since that tracks what a given team keeps getting wrong.
  • SCALE-SPECIFICAt two engineers sharing a desk the template is mostly ceremony, because the conversation already happens and the reviewer has the full context. At fifteen across three time zones, where review is asynchronous and the reviewer has none of the author's context, the fixed shape is doing most of the work.
  • CONTESTEDThe strongest opposing view is that templates produce compliance rather than thought: engineers fill fields to the standard of "accepted", reviewers skim a familiar shape rather than reading, and the visible completeness actively suppresses the question that was not on the form — so a team is safer with no template and a culture of asking, because at least then the absence is felt. This is right wherever the template has become mandatory, which is most places it exists after two years.

Where the depth lives

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

Domains that do not exist yet
  • Testing & Reliability Engineering — the Tests field is a promise about coverage that this domain can specify but not evaluate.