Beyond ADRs: design docs, Diátaxis, runbooks and C4 diagrams Intermediate
Estimated reading time: 6 min read
This page answers: beyond ADRs, which documents a team needs, who each one is written for, and what it looks like when done right.
Design doc / RFC first, ADR after
Section titled “Design doc / RFC first, ADR after”A design doc is a relatively informal document written before coding, to lay out the strategy and trade-offs and ask for review. Many companies call it an RFC.
Malte Ubl describes the structure commonly used at Google:
- Context and scope
- Goals and non-goals
- The actual design
- Alternatives considered
- Cross-cutting concerns (security, privacy, observability)
| Design doc / RFC | ADR | |
|---|---|---|
| Timing | Before the work, while things are fuzzy | When the decision is settled (often at the end of design review) |
| Purpose | Explore options, invite critique, build consensus | Record one decision and its rationale |
| Scope | A whole change, may produce many decisions | Exactly one decision |
| Length | Ubl: around 10–20 pages for large projects, a “mini design doc” of 1–3 pages | 1–2 pages (Nygard) |
| Afterwards | Gradually goes stale; Ubl advises updating it when the design changes mid-implementation | Immutable; supersede to change your mind |
| Main reader | Current reviewers | Future engineers |
Loading diagram…
A design doc may go stale; an ADR never needs updating.
Ubl also says when not to write a design doc: the problem isn’t ambiguous, or the doc is really an “implementation manual” with no trade-offs. In that case write the ADR directly, or just a good PR description.
Diátaxis: organizing the rest of the docs
Section titled “Diátaxis: organizing the rest of the docs”The rest of the docs often break down because everything is mixed: a README that is a tutorial, a reference and an architecture explanation all at once. Diátaxis splits documentation by reader need into four types:
| Type | Diátaxis description | Example for the order service | Sign it’s written wrong |
|---|---|---|---|
| Tutorial | A lesson; the reader learns by doing | README quickstart: clone, docker compose up, create the first order, see the event in Kafka |
Branches like “if you use X then…”; long explanations |
| How-to guide | Directions to reach a real goal, for someone who already knows the job | “Replay outbox events for a time range”, “Rotate the Kafka secret”, runbooks | Re-teaching concepts from scratch |
| Reference | Accurate, complete, neutral technical description | OpenAPI spec, event JSON schemas, env var table with defaults | Opinions, “you should”; hand-written and drifting from code |
| Explanation | Context, reasons, the big picture | ADRs, architecture.md with C4 diagrams, “Why an order has 7 states” |
Step-by-step commands mixed in |
Two practical tips:
- Generate reference from code (OpenAPI from NestJS decorators, the env var table from the config validation schema). Anything hand-written will drift.
- The README is an entry point, not a warehouse. Keep the quickstart short and link to how-tos, reference and
docs/adr/.
Runbooks for on-call
Section titled “Runbooks for on-call”A runbook is a how-to guide for the person paged at 3 a.m., who may not have written the code. Every alert should link to exactly one runbook; an alert without a runbook is an unfinished alert.
Prometheus describes annotations as the place for longer information such as an alert description or a runbook link:
- alert: OutboxBacklogHigh # metric exported by the relay: age (seconds) of the oldest pending row expr: outbox_oldest_pending_age_seconds > 60 for: 5m labels: severity: page annotations: summary: Outbox has a pending row older than 60s runbook_url: https://git.example.com/order-service/-/blob/main/docs/runbooks/outbox-backlog.mdThe matching runbook:
# Runbook: OutboxBacklogHigh
Alert: oldest pending outbox row > 60s for 5 minutes. Dashboard: <link>Owner: Checkout team. Last updated: 2026-09-12 (by Minh)
## ImpactShipping receives OrderPaid late. Payments are NOT affected (see ADR-0012).
## Diagnosis (in order)1. Is the relay running? `kubectl get pods -l app=outbox-relay`2. Is Kafka accepting? Check publish errors on the relay dashboard.3. Is there a "poison" row retrying forever? SELECT id, event_type, created_at FROM outbox WHERE published_at IS NULL ORDER BY id LIMIT 20;
## Mitigation- Relay crash loop: roll the relay back to the previous image.- Kafka down: do nothing to the outbox; the backlog drains when Kafka returns. Notify #inc-kafka.- Poison row: do NOT delete. Move it to the outbox_parked table following <link>, open a ticket.
## EscalationNo improvement after 30 minutes: call Checkout's secondary on-call, then SRE.What makes a runbook usable:
- User impact stated first.
- Copy-pasteable commands, in diagnostic order.
- Explicit list of what not to do.
- A last-updated date and an owner.
Alert thresholds should come from SLOs (see SLI, SLO and error budget).
C4 level 1–2 diagrams for a service
Section titled “C4 level 1–2 diagrams for a service”Simon Brown’s C4 model has four levels: System context, Container, Component, Code. The C4 site says plainly that system context and container diagrams are enough for most teams; only draw a level if it adds value.
A “container” in C4 is a runtime boundary that runs code or stores data, not a Docker container.
Loading diagram…
How to read it:
| Level | How to view the diagram above | Answers |
|---|---|---|
| 1 — System context | Treat the Order system box as a single box |
Who uses it, which systems it talks to |
| 2 — Container | Open the box | Which processes and datastores, and how they communicate |
| 3 — Component | Inside the Order API | Usually not worth drawing by hand: it changes with every refactor |
Rules for keeping diagrams correct:
- Diagrams are code: Mermaid in
docs/architecture.md. - Every arrow states a protocol or meaning.
- A PR that adds a datastore or a new integration must update the diagram.