Common mistakes when writing ADRs, runbooks and READMEs Intermediate
Estimated reading time: 9 min read
This page collects the mistakes most often found when reviewing ADRs, runbooks and READMEs. Each one shows the common version, the version to write instead, and the concrete cost of leaving it as is.
| Group | Mistake |
|---|---|
| ADR | Overwriting an old ADR, Only good consequences, Noun title, no options, ADRs in a wiki, ADRs for everything, Duplicate numbers |
| Operations | Vague runbooks, Alerts without runbooks |
| README | README mixing four types |
Overwriting an old ADR
Section titled “Overwriting an old ADR”Editing ADR-0012 to match reality after moving to CDC
Common PR
# 0012. Publish order events through a transactional outboxA separate relay claims pending rows with FOR UPDATE SKIP LOCKED...We are not using CDC yet. Revisit when throughput exceeds what polling can handle.Debezium reads the outbox table's WAL and publishes to Kafka.PR to make instead
# 0012. Publish order events through a transactional outbox-- Status: accepted+- Status: Superseded by ADR-0031
# 0031. Move the outbox relay to Debezium CDC (new file)+- Supersedes: ADR-0012## ContextOutbox throughput exceeds what polling can handle; the platform team now offers Kafka Connect(exactly the "revisit when..." condition from ADR-0012).Why
- Overwriting erases why polling was chosen. When CDC hits a replication slot problem, nobody knows how the old option was weighed.
- Nygard: old decisions are kept and marked superseded. Only the status and links may be edited.
ADRs with only good consequences
Section titled “ADRs with only good consequences”Consequences that read like a sales pitch
Bad ADR
## Consequences- Never lose events- The system scales better- Modern, standard microservices architectureGood ADR
## ConsequencesGood:- No more paid orders without an event when the process dies midway.Bad:- Still at-least-once: the relay can publish duplicates. New consumers MUST be idempotent by eventId.- Events are delayed by up to one more poll cycle.- The outbox table only grows: needs a cleanup job and an alert for old pending rows.Why
- Unrecorded bad consequences still happen. A new team writes a non-idempotent consumer, the relay publishes a duplicate, and Shipping creates two shipments for one order.
- The next person meets it as a surprise, concludes the decision was wrong, and reverses it blindly. Nygard requires recording all consequences, not just the positive ones.
Noun title, no rejected options
Section titled “Noun title, no rejected options”An ADR that can't answer 'why not X'
Bad ADR
# ADR 15: Isolation level
## DecisionUse READ COMMITTED.Good ADR
# 0015. Decrement stock with READ COMMITTED + SELECT ... FOR UPDATE instead of SERIALIZABLE
## ContextTwo concurrent checkouts can both read stock = 1 and both decrement it (lost update).Options considered:1. SERIALIZABLE for the whole transaction: correct, but requires retrying on serialization failure, and every checkout code path needs a retry loop.2. UPDATE ... SET stock = stock - 1 WHERE stock >= 1: cheapest, but doesn't work when several rows must be read before deciding.3. READ COMMITTED + SELECT ... FOR UPDATE on the rows to be decremented.
## DecisionWe will lock inventory rows with SELECT ... FOR UPDATE in ascending product_id order.Why
- A noun title doesn’t say what was chosen; the ADR index becomes a meaningless list.
- Without rejected options, six months later someone proposes “switch to SERIALIZABLE to be safe”, and the debate starts from scratch.
- MADR makes
Considered Optionsmandatory precisely for this reason.
ADRs in a wiki, separate from the repo
Section titled “ADRs in a wiki, separate from the repo”Code points to a wiki nobody sees during review
Common
// See the decision at https://wiki.example.com/pages/viewpage.action?pageId=884213await this.outbox.insert(tx, event);Write instead
// ADR-0012: events go through the outbox, do NOT call producer.send() directly here.await this.outbox.insert(tx, event);Why
- A wiki separate from the repo: PRs that change code don’t touch the ADR, and reviewers don’t see it during review. Links break when the company changes wiki tools.
- ADRs in
docs/adr/share the code’s version, are greppable, and are reviewed in the same PR.
Writing an ADR for everything
Section titled “Writing an ADR for everything”The decision log turns into a junkyard
Bad decision log
docs/adr/ 0041-rename-variable-orderSvc-to-orderService.md 0042-bump-lodash-4-17-20-to-4-17-21.md 0043-add-index-orders-created-at.md 0044-use-prettier-print-width-100.md 0045-keep-modular-monolith-dont-split-shipping-yet.mdGood decision log
docs/adr/ 0045-keep-modular-monolith-dont-split-shipping-yet.md
# The rest: commit messages, migrations, linter configgit log --oneline a1c3e9f refactor: rename orderSvc to orderService 7b20d4e chore(deps): bump lodash 4.17.21 (security patch) e91f0aa perf: add index orders(created_at) for the daily reportWhy
- 300 files about variable names bury the important ADRs; nobody reads the index anymore.
- Filter with two questions: is it hard to reverse? will others ask about it again? If not, a commit message is enough.
Runbook: “check the logs and handle appropriately”
Section titled “Runbook: “check the logs and handle appropriately””A runbook that's useless at 3 a.m.
Bad runbook
# Outbox alertCheck the relay logs and handle appropriately.If needed, delete the failing rows in the outbox table.Good runbook
# Runbook: OutboxBacklogHighOwner: Checkout team. Last updated: 2026-09-12
## ImpactShipping receives OrderPaid late. Payments are NOT affected.
## Diagnosis1. kubectl get pods -l app=outbox-relay2. SELECT id, event_type, created_at FROM outbox WHERE published_at IS NULL ORDER BY id LIMIT 20;
## Mitigation- Poison row: do NOT delete. Move it to outbox_parked, open a ticket.Why
- No commands, no thresholds: the on-call engineer (who usually didn’t write this code) has to guess, and MTTR drags on.
- “Delete the failing rows” means permanently losing events: the order is paid but Shipping never receives it. A good runbook spells out what not to do.
Alert with no runbook link
Section titled “Alert with no runbook link”An alert that fires but doesn't point the way
Bad alert rule
- alert: OutboxBacklogHigh expr: outbox_oldest_pending_age_seconds > 60 labels: severity: pageGood alert rule
- alert: OutboxBacklogHigh expr: outbox_oldest_pending_age_seconds > 60 for: 5m labels: severity: page annotations: summary: Outbox has a pending row older than 60s for 5 minutes runbook_url: https://git.example.com/order-service/-/blob/main/docs/runbooks/outbox-backlog.mdWhy
- Without
for, the alert fires on the first evaluation over the threshold, even for a brief spike during a deploy: on-call gets woken up for nothing. - Without a runbook link, whoever gets paged has to hunt for docs in the middle of the night. Prometheus describes annotations as exactly the place for alert descriptions and runbook links.
README mixing tutorial, reference and explanation
Section titled “README mixing tutorial, reference and explanation”A README that's a warehouse instead of an entry point
Bad README
# order-service## HistoryIn 2023 the team chose Kafka over RabbitMQ because... (3 pages)## InstallationIf you use a Mac M1 then..., if you use Windows then..., if you use Postgres 14 then...## Env varsDB_URL, KAFKA_BROKERS, OUTBOX_POLL_MS (=500) <- hand-written, code has changed the default to 200Good README
# order-serviceManages the order lifecycle (Ordering bounded context). Does NOT handle shipping.
## Quickstartdocker compose up -d && npm ci && npm run migrate && npm run dev
## Docs- Architecture (C4): docs/architecture.md- Decisions: docs/adr/ (why Kafka: ADR-0004)- Runbooks: docs/runbooks/- Env vars: generated from the config schema, see docs/reference/config.mdOwner: Checkout team, #team-checkoutWhy
- Per Diátaxis, “why Kafka” is explanation (belongs in an ADR), and the env var table is reference (should be generated from code).
- A hand-written env var table with a stale default: a new dev configures per the README, it runs, but behaves differently from production.
- A quickstart that branches everywhere means newcomers can’t get the service running on day one.
Two ADRs with the same number
Section titled “Two ADRs with the same number”Two parallel branches both create 0013
Common
docs/adr/ 0013-use-redis-for-idempotency-keys.md # merged from branch A 0013-split-reporting-schema.md # merged from branch BDo instead
# CI: fail if two ADR files share a number prefixdupes=$(ls docs/adr | grep -oE '^[0-9]{4}' | sort | uniq -d)if [ -n "$dupes" ]; then echo "Duplicate ADR numbers: $dupes" exit 1fiWhy
- “See ADR-0013” now points to two different decisions; links in code and commit messages become ambiguous.
- Whoever merges second renumbers before merging; CI blocks it if they forget. No need to switch to date-based numbering.
Next: go back to the ADR and technical docs overview, or see the Code review cluster.