#Writing a design document
AddedA design document is a decision made in writing before the code exists. It says what problem you're solving, what you're deliberately not solving, the design you propose, the alternatives you rejected and why, and what's still open. Its job isn't to describe the code (the code will do that); its job is to let the right people disagree with you while changing your mind is still cheap. A wrong API shape caught in a doc costs a comment; caught after two sprints in two time zones, it costs a rewrite and a partner migration.
A good mental model: the design doc is the code review you hold before the code. Reviewers can't comment on what they can't see, and on a distributed team they can't see what's in your head or in a call they slept through. Writing it down turns a design into something a developer in Pune, an architect on the client's platform team and a QA lead can each read at their own desk, comment on line by line, and sign off. It's also the most honest prototype you have: if you can't write the "Alternatives considered" section, you haven't considered any.
Malte Ubl's widely read description of the practice, Design Docs at Google (industrialempathy.com, July 2020), gives the usual sections (context and scope, goals and non-goals, the design, alternatives considered, cross-cutting concerns such as security, privacy and observability), suggests 10 to 20 pages for a large project and a 1 to 3 page "mini design doc" for smaller changes, and is just as clear about when not to write one: when the solution is obvious and there are no real trade-offs, or when the doc would only be an implementation manual. The template below follows that shape and adds what a client engagement needs: who decides, by when, and what changes for teams outside yours.
Design doc, ADR, RFC or PR description?
Teams use several written formats for decisions, and choosing the right one is part of the skill:
| Format | Size | Answers | Typical audience |
|---|---|---|---|
| PR description | a few paragraphs | what this change does, why, how it was tested | reviewers of one PR |
| ADR | one page | one decision, its context and its consequences | the team, future maintainers |
| Mini design doc | 1–3 pages | a small cross-team change: proposal, one or two alternatives | a few reviewers outside the team |
| Design doc | 5–20 pages | a significant change: design, alternatives, rollout, risks | several teams, an approver |
| RFC (request for comments) | varies | a proposal open to a wide audience, often an organisation-wide standard | the whole engineering org |
The names vary between companies (some call every design doc an RFC); what matters is that the format's weight matches the decision's.
When a change needs a design doc
| Write a doc when… | Skip it (a ticket or a PR description is enough) when… |
|---|---|
| it changes a contract other teams or partners rely on (a REST or Telnet API, a settings schema, an event format) | it's internal to one module and reversible in one PR |
| more than one team has to build or approve something | one engineer can build and review it in a day or two |
| there are at least two credible designs and the choice has consequences | there's one obvious way and everybody agrees |
| it touches security, data migration, firmware compatibility or the update path | it's a bug fix with a clear root cause |
| the work spans several sprints or several people | it's a spike whose output is itself a short write-up |
# <Title: the change, not the project name>
Status: Draft | In review (comments close <date>) | Decided | Superseded by <link>
Author: <name> Reviewers: <names and teams> Approver: <one name>
Last updated: <date> Tickets: <links>
## 1. Summary
Three or four sentences: the problem, the proposal, what it changes for whom.
## 2. Context
What exists today, with links. What's wrong with it, with evidence (bug counts, support tickets, metrics).
Assume the reader is a capable engineer who has never seen this part of the product.
## 3. Goals
- Measurable outcomes. "Saves are atomic" beats "better saving".
## 4. Non-goals
- What this deliberately does NOT do, so nobody reviews it for that.
## 5. Proposed design
### 5.1 Overview (a diagram)
### 5.2 API / interface changes (request and response examples)
### 5.3 Data and storage
### 5.4 UI changes
### 5.5 Compatibility and migration (old clients, old firmware, partners)
## 6. Alternatives considered
For each: what it is, why it's attractive, why it loses here.
## 7. Cross-cutting concerns
Security · privacy · performance on the device · observability · accessibility · localisation
## 8. Testing and rollout
How it's tested (unit, contract, integration, lab), how it's released (flags, stages), how it's rolled back.
## 9. Risks and open questions
| # | Question or risk | Owner | Needed by |
## 10. Decision log
| Date | Decision | Who decided | Why |
## 11. Sign-off
| Role | Name | Status |- The header is for the reader who arrives from a link a year later.
Statustells them whether this is a proposal, a decision or history;Superseded bystops someone implementing a design that was replaced.Approveris one name, not a committee: a doc with five approvers is a doc nobody can close (see Running technical discussions and making decisions). Comments close <date>makes the review asynchronous on purpose. Across a US–India split there's no hour when everybody is fresh. A comment window of a few working days lets every reviewer read it in their own morning, and the meeting, if there is one, only covers what the comments couldn't settle.- Context comes before the design and carries evidence. "Two admins overwrite each other" is an opinion; "14 support tickets in Q3 traced to concurrent saves" is a reason. The context section is also where you link the existing API docs and code, because on a team with thin documentation the doc is often the first written description of how the current system works.
- Non-goals save more review time than any other section. Without them, reviewers ask for everything adjacent ("while you're there, can it also…?") and the doc grows into a project. Writing "Telnet commands are out of scope; separate doc" ends that thread before it starts.
- Alternatives considered is the section reviewers trust you by. If every alternative is a straw man, they assume you picked first and justified later. Give each one its real strength, then the specific reason it loses here.
- Open questions have owners and dates; the decision log records what was settled and by whom. Together they turn the doc from an essay into a working tool: the India team can see which questions block them, and nobody re-opens a decision without reading why it was made. This is the same discipline as an architecture decision record, but for a whole change rather than one decision.
A filled example: a device web UI and API change
Here's the template filled in for a change typical of the role: the web UI and REST API of a video-conferencing room device. This is illustrative. The device, the endpoints and the numbers are made up to show how a doc reads; they are not Poly's API or any real product's internals.
# Settings v2: atomic, conflict-safe settings saves for the device web UI
Status: In review (comments close Thu 12 Nov)
Author: <web UI/API lead> Approver: <client platform architect>
Reviewers: API dev lead (India), web UI dev lead (India), Android platform team, QA lead, security
Tickets: WEB-412, API-388
## 1. Summary
The device web UI saves each changed setting with its own request, so a save can half-apply and two
admins can silently overwrite each other. We propose one PATCH request per save that is validated as a
whole, applied atomically, rejected if someone else saved first, and tells the UI which changes need a
restart. The existing v1 endpoints keep working for partners and older firmware.
## 2. Context
- The settings page calls PUT /api/v1/settings/{key} once per changed field (the network page sends up to 9).
- If request 5 of 9 fails validation, requests 1–4 are already applied: the device is half-configured.
- There is no concurrency check: the last writer wins. Support traced 14 tickets in Q3 to this.
- Some settings only take effect after a restart; the UI can't tell which, so it says "some changes may
need a restart" after every save.
- Partner integrations and the cloud management service use the v1 endpoints; firmware 4.x devices will
stay in the field for years and never get v2.
## 3. Goals
- One save = one request, validated in full, applied all-or-nothing.
- A save based on stale data is rejected, and the UI shows what changed.
- The response lists exactly which saved settings need a restart.
- v1 behaviour is unchanged for existing callers.
## 4. Non-goals
- New settings, or changes to which settings exist.
- Telnet / SSH command changes (tracked separately in API-401).
- Cloud management changes; the cloud service keeps using v1 until it opts in.
## 5. Proposed design
### 5.1 Overview
Web UI → PATCH /api/v2/settings → validate all → apply in one config-store transaction → respond.
### 5.2 API
GET /api/v2/settings
200, ETag: "cfg-1742", body: { "network": { "dhcp": true, ... }, "audio": { ... } }
PATCH /api/v2/settings
Content-Type: application/merge-patch+json (RFC 7396)
If-Match: "cfg-1742"
body: { "network": { "dhcp": false, "ipv4": "10.0.4.20" }, "audio": { "echoCancel": true } }
200 ETag: "cfg-1743"
{ "applied": ["network.dhcp", "network.ipv4", "audio.echoCancel"],
"restartRequired": ["network.dhcp", "network.ipv4"] }
412 Precondition Failed: someone saved since "cfg-1742"; body says which keys differ
422 Unprocessable Content, RFC 9457 problem details, one entry per invalid field; nothing applied
428 Precondition Required: If-Match missing
POST /api/v2/settings/validation same body; validates only, never applies (dry run for the UI)
### 5.3 Data and storage
Needs an all-or-nothing multi-key write in the device's config store. If the store can't do that
(open question Q1), write to a staging copy and swap it in one step.
### 5.4 UI changes
One Save button per page; a summary of pending changes before saving; on 412, show the other admin's
values next to yours and let the user choose; a persistent "Restart needed for: …" banner.
### 5.5 Compatibility
v1 endpoints stay, re-implemented on top of the v2 apply path, so both share validation. v2 is only
offered by firmware that advertises "settings.v2" in GET /api/capabilities; the UI falls back to v1.
## 6. Alternatives considered
A. Keep per-field PUTs and retry failures in the UI. Smallest change; still not atomic, still last-writer-wins.
B. PUT the whole settings document. Atomic and simple, but every save sends ~40 KB, and an older UI
would reset settings it doesn't know about (newer firmware adds them).
C. JSON Patch (RFC 6902). More expressive (array operations, test ops), but harder for partners to
produce by hand, and no setting today is an array that needs element edits. Merge Patch is enough.
Known limitation: Merge Patch can't set a value to null (null means "remove"); no setting is nullable today.
## 7. Cross-cutting concerns
- Security: same roles as v1 (admin only); every save writes one audit entry with the diff and the user.
- Device performance: one transaction instead of up to 9 requests; to be measured on the lowest-spec model.
- Observability: count 412 and 422 responses; a rising 412 rate means admins are colliding.
- Accessibility: the conflict view must work with a keyboard and a screen reader.
## 8. Testing and rollout
- Contract tests generated from the OpenAPI spec; a concurrency test (two saves, one stale → 412).
- Lab: power-cut during save → device boots with either the old or the new settings, never a mix.
- Behind a feature flag on 5.x builds; lab devices → internal beta fleet → general release.
- Rollback: turn the flag off; the UI falls back to v1 automatically.
## 9. Risks and open questions
| # | Question or risk | Owner | Needed by |
| Q1 | Does the config store support multi-key transactions? | Android platform| 10 Nov |
| Q2 | Authoritative list of restart-required settings | Android platform| 13 Nov |
| R1 | Cloud service sees ETag changes it didn't cause; may resync more | Cloud team | before beta |
## 10. Decision log
| Date | Decision | Who | Why |
| 3 Nov | Merge Patch over JSON Patch | API lead + author | Partners write it by hand; no array edits needed |
## 11. Sign-off
| Role | Name | Status |
| Approver (client platform) | | pending |
| Security | | pending |- The summary is written last and read first. Four sentences tell a busy approver the problem (half-applied saves, silent overwrites), the proposal (one atomic PATCH with a conflict check), and the blast radius (v1 unchanged). Most stakeholders read only this and section 9; write them for that.
- The context proves the problem with things a reviewer can check: the request count per page, the half-applied failure mode, the 14 support tickets, and the fleet fact that 4.x devices never get v2. That last line is what makes compatibility a goal rather than an afterthought, and it's specific to device platforms: you can't upgrade every client at once, because some clients are firmware in a drawer.
- The API section shows requests and responses, including the failures.
If-Matchwith anETaggives optimistic concurrency (RFC 9110's412 Precondition Failedwhen the tag is stale, and428 Precondition Requiredfrom RFC 6585 when it's missing);422with RFC 9457 problem details lists every bad field so the UI can mark them all at once; the validation endpoint gives the UI a dry run. Reviewers argue about shapes, so give them the shapes. - Alternatives are given their real strengths. Whole-document
PUTis simpler and atomic; it loses on payload size and on a subtle compatibility bug (an older UI wiping settings it doesn't know). JSON Patch is more expressive; it loses on partner ergonomics. The Merge Patch limitation (no way to setnull) is written down where a reviewer will find it, rather than discovered by a partner. - Open questions are owned by other teams and dated. Q1 and Q2 belong to the Android platform team, and the whole design depends on Q1. Putting a date on it is how you turn "waiting on platform" into something a status update can report and, if needed, escalate (see Status updates and escalation).
- The decision log already has an entry while the doc is still in review. Small decisions get made during drafting; logging them with the reason stops the same argument recurring in every review round, and tells the India-based leads which questions are settled so they can start the parts that don't depend on Q1.
The meeting is the exception, not the default. When you do hold one, send the agenda as a list of unresolved comment threads, give everyone ten minutes at the start to reread them, and end with each thread marked resolved, accepted or turned into an open question with an owner. Record the outcome in the decision log the same day; the India-based team will read it in their morning and start work, so a decision that only lives in the meeting is a decision they don't have.
As the lead you'll review more design docs than you write, many of them from engineers on your India-based team who are writing their first. The review is both quality control and teaching, so it has two passes.
First pass: is it the right problem and the right shape? Read only the summary, context, goals and non-goals. Ask:
- Is the problem real and evidenced, or assumed?
- Would I know, from the goals, whether the finished work succeeded?
- Is anything missing from the non-goals that reviewers will otherwise demand?
- Does someone outside the team (partners, the platform team, the cloud service) depend on this? Are they reviewers?
If the first pass fails, stop and say so in one comment ("Before the details: I think the goal should be X, because Y. Can we agree that first?"). Line comments on a design that's solving the wrong problem waste both people's time.
Second pass: does the design hold up? Now read the design and alternatives:
- Contracts: are request and response examples given, including errors? What does an old client or old firmware see?
- Failure: what happens on a timeout, a reboot mid-operation, a power cut, two users at once?
- Device cost: memory, CPU, connections, flash writes on the lowest-spec model. Measured or guessed?
- Security: who can call it, what's logged, what's exposed on the network?
- Testing and rollout: can it be tested in CI, does it need the lab, how is it turned off?
- Alternatives: is the strongest alternative given a fair hearing?
Write comments the way you'd want them on your own doc: label the weight ("blocking:", "question:", "suggestion:", "nit:"), explain the reason, and propose something when you can. "Blocking: a 412 here means the UI loses the user's edits; could the response include the current values so the UI can show both?" teaches; "This won't work" doesn't.
Your team is asked for four changes this sprint. (a) Rename an internal React component and its file. (b) Add a restartRequired field to every settings response of the public REST API. (c) Fix a bug where the language dropdown shows the wrong default. (d) Replace the polling status page with a WebSocket stream from the device. Which ones get a design doc, and how big?
Show answer
(b) and (d) get a doc; (a) and (c) don't. (b) can be a 1–2 page mini doc; (d) needs a full one.
- (a) is internal to the web UI, one PR, trivially reversible, with no consumer outside the team: a PR description is enough.
- (b) looks small but changes a public contract: partners and the cloud service parse those responses, and older firmware won't have the field. A short doc settles the field name, when it's present, what older clients see and how it's documented in the OpenAPI spec. That's a mini design doc: one decision, a few reviewers, but written down because outsiders depend on it.
- (c) is a bug with a clear cause and fix; a ticket and a test are the right paperwork.
- (d) changes device resource use (open connections on a constrained device), adds a new interface the platform team must support, has real alternatives (SSE, long polling, smarter polling) and needs a reconnection story for reboots. That's every row of the "write a doc" table: a full doc with alternatives, device performance and rollout sections.
- The rule underneath: size the doc to the cost of being wrong and the number of people outside your team who depend on the decision, not to the number of lines of code.