Writing a design doc
How do you write a design down so others can review it, and so you remember why you chose it?
Why does nobody remember the random codes?
In October the kitchen hires its first engineer from outside the family. In the second week a pull request arrives against the links service. The change is small and sensible: short codes would come from a counter instead of random characters, seven characters instead of eight, and no retry when a random code turns out to be taken already. The description asks one question: random codes take more code to get right, and a counter never collides, so was there a reason for choosing random codes?
There was one, and nobody on the team can say what it was. A search of the team chat finds a thread from the spring: forty messages, a screenshot of a tracking page, and the words “fixed, see PR”. The pull request says “switch to random codes”. Nothing says why.
In the end the person who answers the customer phone remembers it: a customer had changed one letter in her tracking link and saw somebody else’s address. The accountant points out the contrast: every rupee she moves has a reason written next to it, yet the team changed what a customer’s link can show to strangers and wrote down nothing.
In general the proposal is fine. A public shortener whose links open public pages might well choose a counter. Here the links open pages with names and addresses, so a code that cannot be guessed is what keeps strangers out. Only the lost context explains that.
Without that context the team has two bad choices. Keep the old decision blindly, and answer a good question with “because we said so”. Or reverse it blindly, and reopen a privacy hole a customer has already found once.
Every design so far has ended in a decision record. Before reading on, think about where this team kept its records. The answer is in one person’s head, and next week the biggest feature yet starts: weekly subscriptions, which touch orders, payments and the kitchens.
Pause and predict A newcomer asks why the codes are random, and the reason lives only in a forty-message chat thread. What would have saved the team?
A decision without its reason
Every design is a pile of decisions, and most of them are invisible in the code. The code says the codes are random. It cannot say that a counter was considered, why it lost, or what would have to change for it to win.
Michael Nygard named the trap in 2011. A newcomer who meets a decision with no recorded rationale can only accept it blindly (and keep paying for it after its reason has gone) or change it blindly (and break whatever it was protecting). “It’s better to avoid either blind acceptance or blind reversal,” he wrote.
Chat threads and pull requests do hold some of the reasoning, but it is scattered, hard to search by topic, and mixed with everything else. They record that something changed, rarely what else was weighed.
Two documents fix this, at two sizes:
- A design doc, written before a significant piece of work: the problem, the goals and non-goals, the design, the alternatives considered, and the concerns that cut across it (security, privacy, cost, failure). Others review it while changes are still cheap.
- An architecture decision record, one per significant decision: its context, what was chosen, what was turned down, and what it costs. Records are short and numbered, kept next to the code, and never deleted: a newer record supersedes an old one, and both stay.
A mental model
- Design doc
- A document written before significant work that explains the problem, the chosen design, and the trade-offs, so others can review it while changing it is still cheap.
- For example: Three pages on weekly subscriptions: goals and non-goals, the scheduler and the data, the alternatives rejected, how it fails, how it rolls out, and what each order costs.
- Y-statement
- A one-sentence decision record: in the context of …, facing …, we chose … and neglected …, to achieve …, accepting ….
- For example: The record from the URL shortener (lesson 1.5): “…we chose 8 random base62 characters and neglected a base62 counter, to achieve a 1 in 30 million chance per guess, accepting one more character per link.”
Sizes and lifecycles
| Document | Size | Lives | When it changes |
|---|---|---|---|
| Decision record (Nygard) | 1 to 2 pages | In the repository, numbered in order, numbers never reused | Never edited in substance; a new record supersedes it and both stay |
| Y-statement | 1 sentence | At the end of a design doc, or as an ADR’s summary | As above |
| Mini design doc (Ubl) | 1 to 3 pages | Shared document, linked from the code | Updated while the work is unshipped; afterwards, amended or left as history |
| Design doc, larger project (Ubl) | 10 to 20 pages | As above | As above; split large problems into several docs |
A record has a status: proposed, accepted, and later deprecated or superseded, with a pointer to the record that replaced it. The old one stays, so the history of a decision can be read in order.
A document for everything, or for the decisions that matter?
One reaction to the lost reason is a design doc before every feature from now on. The other is that paperwork slows good teams down, and most changes need none. Both have a cost.
Most changes need no document: Ubl says to skip one when the solution is unambiguous, and admits that “in practice we humans are bad at updating documents”. The expensive decisions do need writing before they are made. So the deciding question is how costly a change would be to reverse.
The first record written is the one that was lost: short codes are random, the counter was neglected, and the decision is worth revisiting only if links stop opening private pages. The newcomer reads it and withdraws the pull request, then proposes the keyed scramble from lesson 1.5 as a new record, status proposed, so the next review has both on the table.
What to remember
- A decision without its reason leaves only blind acceptance or blind reversal.
- A design doc is written before costly work, for reviewers: goals and non-goals, the design, alternatives considered, failure, rollout and cost.
- A decision record is written for whoever inherits the system: context, choice, what was neglected, what it costs. Numbered, kept in the repository, superseded and never deleted.
- Review for decisions and risks: a missing number, a missing alternative, a missing reason. Style can wait.
- Write documents for changes that are costly to reverse; let small, obvious changes explain themselves in code and pull requests.
Write the reason next to the decision while you still remember it.
In the real world
The lab reviews a four-section draft with seven planted gaps. Real reviews are conversations: comments disagree, authors push back, and the best reviewers also say what is strong, so the author keeps it. Larger organisations add formal steps (a review meeting, a security or privacy sign-off before launch) that a team of four does not need yet.
The shape scales down well. A team of four can keep its records as numbered Markdown files next to the code, review a three-page doc in one conversation, and still answer a newcomer’s question in a minute. The capstone (lesson 8.5) asks you to defend a design on your own; writing its decisions down this way is half of the defence.