Step 18 of 24 · optional

Design docs & ADRs

Write a design doc that gets a decision made before code is written, and record architecture decisions in short ADRs that explain why.

14 min 7 sections

The most expensive bugs are design bugs: a data model that can't represent next quarter's requirement, an integration that can't scale, two teams building the same thing differently. They're cheapest to fix before any code exists, and the tool for finding them then is writing. A design doc forces you to think a problem through and lets others poke holes in your plan in an hour instead of after a month of implementation. An architecture decision record (ADR) preserves why a choice was made, long after the people who made it have moved on. This lesson covers when to write each, what goes in them, and how to get useful reviews.

Why write it down

Writing is thinking made visible. A plan that sounds complete in your head ("we'll just add a status column and a cron job") reveals its gaps the moment you write the migration plan, the failure modes and the rollout. A document also:

  • Scales review. Ten people can read and comment asynchronously across time zones; a meeting needs everyone at once, and the loudest voice wins.
  • Surfaces disagreement early. Another team says "we're deprecating that API next quarter" before you've built on it.
  • Becomes onboarding material. New engineers learn not just what the system is, but why.
  • Builds your reputation. At most companies, clearly written designs are a large part of how engineers demonstrate senior-level impact — the growing to senior lesson comes back to this.

Not everything needs a doc. A good threshold: write one when the work takes more than about two weeks, touches several teams or services, changes a public API or data model, or is hard to reverse. A one-day bug fix needs a good PR description, not a design doc.

Anatomy of a design doc

Formats vary by company (RFC, tech spec, one-pager), but strong docs share a structure. Keep it as short as the problem allows — five pages is plenty for most.

1. Context and problem. What's happening now, why it's a problem, and for whom. Use numbers: "checkout p99 latency is 2.4 s, 40% of it in the promotions call" beats "checkout is slow".

2. Goals and non-goals. Goals are measurable outcomes ("p99 under 800 ms"). Non-goals are just as important: they explicitly say what you're not solving, which prevents scope creep and endless review comments. "Non-goal: redesigning the promotions engine; we'll only change how checkout calls it."

3. Proposed design. The core of the doc. Describe the architecture, data model, APIs and key flows. A diagram helps — boxes and arrows for components, a sequence diagram for an interaction:

DiagramCheckout with cached promotions
ClientCheckoutCachePromotions1POST /checkout2GET promos:cart-hash3miss4Evaluate(cart)5discounts6SET promos:cart-hash (60s TTL)7200 order summary

Show the API shapes and schema changes concretely — reviewers find problems in specifics, not in prose.

4. Alternatives considered. At least two real alternatives, and why you didn't choose them. This is the section reviewers trust most: it shows you explored the space instead of picking the first idea. "Do nothing" is often a legitimate alternative to include.

5. Cross-cutting concerns. Walk through the ones that matter for this change: security and privacy, failure modes and what users see when things break, observability (metrics, alerts), performance and cost, data migration, and backward compatibility.

6. Rollout and testing plan. How you'll ship it safely: feature flag, percentage rollout, migration steps using expand–contract, how you'll know it's working, and how to roll back.

7. Open questions. What you don't know yet. Listing them invites help rather than looking like a weakness.

Writing so people actually read it

  • Lead with the conclusion. A one-paragraph summary at the top — problem, proposal, main trade-off — lets busy reviewers decide whether they need the details.
  • Write for a reader outside your team. Define acronyms; link to the services you mention.
  • Prefer tables and lists for comparisons, prose for reasoning.
  • Be explicit about trade-offs. Every design gives something up. Saying "this adds 5 ms to every request in exchange for removing the nightly batch job" shows judgment; pretending there's no cost invites distrust.
  • Use numbers. Load, data size, latency budgets, cost. A back-of-the-envelope estimate is far better than "it should scale".

Getting a useful review

  1. Share a rough draft early with one or two people before the wide review. Fixing the direction at 30% complete is cheap; at 100% it's demoralising.
  2. Name your reviewers and what you need from each: "Rina — the payments integration; Dimas — the migration plan." Docs shared with "any feedback welcome" get little.
  3. Set a deadline for comments.
  4. Resolve every comment: accept it, or reply with why not. Unresolved threads become surprise objections later.
  5. Hold a short review meeting only for the remaining disagreements, not to read the doc aloud.
  6. Record the decision at the top: approved, by whom, on what date, and any conditions.

When you're the reviewer, focus on what matters: unclear requirements, missing failure modes, risky migrations, and simpler alternatives. Leave wording nits for last, or out.

Architecture decision records

A design doc describes a large piece of work. An architecture decision record captures one significant decision in a page or less, so that a year from now someone asking "why on earth do we use X?" can find out. The common format comes from Michael Nygard:

text
# ADR 0012: Use PostgreSQL SKIP LOCKED as the job queue

Date: 2026-03-14
Status: Accepted (supersedes ADR 0007)

## Context
Background jobs (emails, webhooks, report exports) run at ~20/s peak.
ADR 0007 introduced RabbitMQ for them; the team has no RabbitMQ expertise,
and two incidents this quarter came from queue misconfiguration.

## Decision
Store jobs in a Postgres table and claim them with
SELECT ... FOR UPDATE SKIP LOCKED. Remove RabbitMQ.

## Consequences
+ One fewer system to operate; jobs can be enqueued in the same
  transaction as the business write (no lost or phantom jobs).
- Throughput ceiling of a few thousand jobs/s on our database size;
  revisit if we approach 500/s sustained.
- Workers poll; we accept up to 1 s of pickup latency.

The rules that make ADRs useful:

  • One decision per record, numbered, stored in the repository (often docs/adr/) so it's versioned and reviewed like code.
  • Immutable once accepted. If the decision changes, write a new ADR that supersedes the old one, and mark the old one as superseded. The history of reasoning is the point.
  • Consequences include the downsides, and ideally a trigger for revisiting.
  • Write it when you decide, not months later. A two-paragraph ADR written today beats a perfect one never written.

ADRs pair naturally with design docs: the doc explores and proposes; the ADRs record the decisions that came out of it.

Practice

Your team wants to replace polling with WebSockets for live order-status updates in the customer app. Write the outline of a design doc for it: a one-paragraph summary, three goals, two non-goals, two alternatives with one-line reasons for rejecting them, and three cross-cutting concerns you would need to address.

Show solution

Summary: The customer app polls GET /orders/{id} every 5 seconds while an order is active, producing 60% of API traffic and up to 5 seconds of status delay. We propose pushing status changes over a WebSocket connection from a new realtime gateway, fed by the existing OrderStatusChanged events, with polling kept as a fallback. This cuts API load and delivers updates in under a second, at the cost of operating a stateful connection tier.

Goals:

  1. Status updates reach the app within 1 s (p95) of the event.
  2. Reduce order-status API requests by at least 80%.
  3. No user-visible regression when the realtime path fails (automatic fallback to polling).

Non-goals:

  1. Realtime updates for screens other than active order tracking.
  2. Replacing push notifications for when the app is in the background.

Alternatives considered:

  • Server-sent events: simpler and sufficient for one-way updates, but our mobile HTTP client and load balancer handle long-lived SSE poorly; worth revisiting for web.
  • Longer polling interval (15 s): cuts load cheaply, but makes the delay worse, the opposite of the product goal.

Cross-cutting concerns:

  • Scaling and failure: connections are stateful; how many per gateway instance, how they're rebalanced on deploy (graceful drain), and what clients do on disconnect (reconnect with jitter, then fall back to polling).
  • Security: authenticating the WebSocket handshake, and ensuring a user only receives events for their own orders.
  • Observability: metrics for open connections, message delivery latency and fallback rate, with alerts.

The polling, SSE and WebSockets note covers the transport trade-offs in more depth.

Key takeaways

  • Write a design doc for work that is large, cross-team, or hard to reverse — before writing code.
  • A strong doc covers context, goals and non-goals, the design, real alternatives, cross-cutting concerns, rollout, and open questions.
  • Lead with a summary, use numbers and diagrams, and state trade-offs explicitly.
  • Get early feedback from a few people, name reviewers and deadlines, resolve every comment, and record the decision.
  • ADRs capture one decision each — context, decision, consequences — stored with the code and superseded rather than edited.

Check yourself

Quiz

0/4 correct · 4 left

  1. 1.Why is a 'non-goals' section valuable in a design doc?

  2. 2.Which section of a design doc do experienced reviewers typically trust most as a sign of a well-considered proposal?

  3. 3.Six months after ADR 0007 chose RabbitMQ, the team decides to switch to a Postgres-backed queue. What should happen to ADR 0007?

  4. 4.Which piece of work most clearly warrants a design doc?

Concepts in this lesson

One-page notes on each idea, for review or a quick lookup later.

Finished reading?

Mark it complete to fill in your roadmap.