Architecture Decision Records (ADRs) in practice
Monday, 10:30: Theo, two weeks into the job, opens a pull request deleting the outbox table as "unused complexity". Six big decisions this quarter, zero written down, and three engineers spend two hours digging through chat to explain why it exists.
A short note that says what you decided and why, so nobody has to guess a year from now.
- Problem
Two years later nobody remembers why the team chose PostgreSQL, why bookings go through a queue, or why the payment port exists, so people either fear changing anything or reverse good decisions blindly.
- Why the simple system fails
Decisions live in meetings, chat threads and one senior engineer's head. Wikis drift out of date, and the reasoning leaves when people leave.
- Engineering concept
Record each significant decision as a short, immutable document: context, drivers, options, decision, consequences. Change decisions by superseding, never by rewriting history.
- Technology
Markdown ADRs in the repo (docs/adr/NNNN-title.md), templates such as Nygard-style or MADR, review through pull requests, an index of statuses.
- Trade-offs
A little writing effort per decision versus repeated debates, onboarding cost and accidental reversals. Over-documenting trivial choices wastes that effort.
The problem
2 min- TheoPR #2214: remove the outbox table. It looks like unused complexity.
- Omar · On-call SREThat's how we lost 9 bookings in the spring. Two hotels resold rooms.
- TheoHow was I supposed to know? I searched the docs.
- Lina · Senior engineer, your mentorThere's nothing to find. Last episode of the module: you lead this one.
The big idea
4 minThe code shows what you built; an keeps why, what you rejected, and what would change your mind, one short page per decision, never rewritten.
A captain doesn't rewrite yesterday's logbook entry when the weather changes: she writes a new one, "changed course because of the storm". The next captain can follow every turn, and why.
How it works
5 minAn Architecture Decision Record is one or two pages recording one significant decision, kept in the repo. Worth writing when the decision is a one-way door, crosses teams, or will be questioned later.
- 1Spot the decision
Expensive to reverse, crosses teams, or likely to be questioned? Write one. A date library? Don't.
- 2Draft as Proposed
docs/adr/0004-booking-events-outbox.md: context with numbers, (the forces that matter most: correctness, cost, latency), 2+ real options, decision, consequences, "revisit if". - 3Review in a pull request
Affected teams challenge numbers and add options before any code is written.
- 4Accept and index
Status moves Proposed → Accepted, with a date; listed in
docs/adr/README.md. - 5Supersede, never edit
When a revisit trigger fires, a new ADR replaces it. The old one stays, marked with a link.
Watch a decision being made
4 minYou write ADR-0004: booking events through an outbox (Lina reviews)
Record why BookingConfirmed goes through an outbox, so the next engineer who wants to remove it reads the reason in two minutes.
- 1Check it deserves one, write the context with numbers
Removing it loses events; it crosses teams; it's already been questioned. Context: checkout p95 2.8 s, ~50 failed bookings a day, 9 events lost by commit-then-publish, 2,500 bookings a day.
Why: ADRs for trivial choices bury the important ones. Numbers freeze the assumptions so a reader in 2028 can check them.
- 2Name the drivers, list honest options
Drivers: never lose a booking event; nothing new to operate; checkout latency. Options: commit-then-publish, outbox + relay, managed Kafka, each with real pros and cons.
Why: Drivers make the weighting explicit. Strawmen destroy trust; show what a reasonable colleague would propose.
- 3State the decision with its costs
Outbox in PostgreSQL, relay to our workers. Consequences: no lost events, p95 ~0.5 s; every consumer must be idempotent; the relay adds light load.
markdown## Consequences + No lost events; checkout p95 ~0.5 s. - Every consumer must be idempotent (processed_events table). - Removing the outbox brings back lost events (see incident 2026-05-07).Why: The negatives are what stop the next "unused complexity" PR, and prepare people for duplicates.
- 4Add a revisit trigger, link it from the code
"Revisit if we extract a service that consumes booking events, or pass 6 consumers." On the outbox table:
-- see docs/adr/0004.Why: The trigger says when changing it is right; the link puts the reason where people stumble on the decision.
In real life
3 minSpotify: lightweight ADRs
- Its engineering blog publicly recommended ADRs and when to write one
- Short, one decision each, close to the code
Fits frequent, team-level technical decisions.
Amazon: six-page narrative memos
- Publicly described replacing slide decks with written narratives
- Long documents for big business bets; slow to write, deep reasoning
Fits rare, large decisions needing many people aligned.
What goes wrong
3 minThe history of reasoning disappears.
A new ADR that supersedes it; change only the old status line.
It reads like a sales pitch, and readers stop trusting ADRs.
Real alternatives with honest pros; list what gets harder.
Hundreds of trivial records bury the important ones.
Only decisions that are costly to reverse, cross teams or will be questioned.
Trade-offs
3 min| Approach | Fits when | You pay |
|---|---|---|
| Lightweight ADRs in the repo | Almost always beyond a handful of engineers | A habit someone must nudge |
| Heavy design docs / review boards | Multi-quarter, multi-team or compliance decisions | Slow; teams route around them |
| Nothing written | Throwaway prototypes | Re-debates and accidental reversals |
- Omar · On-call SREMake it mandatory: CI fails any PR touching
infra/or a module API without an ADR link. - Karim · CTOThat's 20 PRs a week. People will write empty ADRs to get green builds. Keep it a habit.
- Omar · On-call SREHabits are how we ended up with zero ADRs for six decisions.
Your job as tech lead
4 minClaude Code can write the code for this. You still own the decisions, and you have to know what good looks like to accept or reject its work.
- Which decisions get an ADR (the outbox, the PaymentPort, the cap of 50), and which don't (a date library).
- The decision, its drivers and its honest downsides. Claude can draft from your notes; the choice is yours.
- How Claude finds the records: a line in
CLAUDE.mdto readdocs/adr/before changing anything an ADR covers.
Draft docs/adr/0006-photo-resizing-on-functions.md with the same sections as 0004. Use only these facts: ~4,000 photos on Sunday evenings, ~$0.60 a month on functions versus ~$120 for two extra servers, PostgreSQL max 200 connections with ~100 used, concurrency cap 50, and the incident where the cap was removed and bookings failed for 6 minutes. List a real downside of the chosen option. Mark anything unsure as TODO. Add a comment next to the cap pointing to the ADR, and a CLAUDE.md line about docs/adr.
- No numbers you didn't give. Anything you can't trace is invented: remove it.
- Real options, real downsides, and a measurable revisit trigger, not "if needs change".
- The code links to the record, and when a decision changes, Claude writes a new ADR.
- It rewrites an accepted ADR to match new code instead of superseding it.
- The record lists one option, or only benefits.
- It deletes the outbox or raises the cap as "unused complexity" without checking
docs/adr/.
Check yourself
2 minNext episode, new module: Maya wants guest reviews in four weeks, and within ten minutes the whiteboard shows Kafka, Cassandra and five new services for about 750 reviews a day.
Mark it complete to update your skill map.
Go deeper (optional)You don't need this to understand the lesson. Open it when a project needs the details: Technical deep dive, Interactive simulation, Visual diagram, Analogy, Check your understanding, Practice ladder, Explain it yourself.
Technical deep dive
You write the retroactive ADR for the outbox; Lina only reviews. Here's the method, then the finished record.
Writing ADR-0004: booking events through an outbox
Record why StayHub publishes BookingConfirmed through an outbox, so the next engineer who wants to remove it reads the reason in two minutes.
- 1Check it deserves an ADR
Removing it would bring back lost events (a hotel resold rooms); it touches Booking and every consumer team; it has already been questioned. Three yeses.
Why: ADRs for trivial choices bury the important ones. This one is a one-way door that crosses teams.
- 2Write the context with numbers
Checkout p95 2.8 s with 4 inline side effects; ~50 failed bookings a day; the commit-then-publish version lost 9 events in one deploy; 2,500 bookings a day; all consumers in one app.
Why: Numbers freeze the assumptions. A reader in 2028 can check whether they still hold.
- 3Name the drivers
- Never lose a booking event (hotels must know). 2. Nothing new to operate for one on-call rotation. 3. Checkout latency.
Why: Drivers make the weighting explicit; otherwise the winner "wins" on whatever criteria the author picked.
- 4List real options, honestly
Commit-then-publish; outbox + relay; managed Kafka. Each with real pros and cons, including the downsides of the chosen one (duplicates, relay load).
Why: Strawmen destroy trust. A reader should see the options a reasonable colleague would propose.
- 5State decision and consequences
Decision: outbox in PostgreSQL, relay to our workers. Consequences: no lost events, p95 ~0.5 s; consumers must be idempotent; removing the outbox brings back lost events (link the incident).
Why: The negative consequences are what stop the next "unused complexity" PR.
- 6Add a revisit trigger and link it
"Revisit if we extract a service that consumes booking events, or pass 6 consumers." Add a comment on the outbox table:
-- see docs/adr/0004.Why: The trigger tells people when changing it is right. The link puts the reason where people stumble on the decision.
# ADR-0004: Publish booking events through an outbox
- Status: Accepted
- Date: 2026-05-12
- Deciders: Booking team, Hotel Partners team, Lina (review)
## Context
Checkout called invoice, email, loyalty and the hotel's channel manager inline:
p95 2.8 s and ~50 failed bookings/day (channel-manager timeouts after the card was charged).
A first event version (commit, then publish) lost 9 events in one deploy (2026-05-07);
2 hotels resold rooms. ~2,500 bookings/day; all consumers live in the monolith.
## Decision drivers
1. Never lose a booking event: hotels must learn about every booking.
2. Nothing new to operate for one on-call rotation.
3. Checkout latency.
## Considered options
- Commit, then publish: simplest. Loses events when the process dies in between.
- Outbox table + relay to our workers: no lost events, nothing new to run.
Delivery is at least once; the relay polls the database.
- Managed Kafka: replay, high throughput, ready for services.
A new system and bill for 2,500 bookings/day; schema management.
## Decision
Write BookingConfirmed/BookingCancelled to an outbox table in the same transaction
as the booking. A relay publishes to our worker queue.
## Consequences
+ No lost events; checkout p95 ~0.5 s.
- Every consumer must be idempotent (processed_events table).
- Relay adds light load on PostgreSQL; alert if the oldest unsent row is > 60 s.
- Removing the outbox brings back lost events (see incident 2026-05-07).
## Revisit if
- We extract a service that consumes booking events, or
- More than 6 consumers, or teams need to replay weeks of history.| Section | Purpose | Smell when missing or weak |
|---|---|---|
| Status + date | Tells readers whether it still applies | People act on decisions superseded years ago |
| Context with numbers | Freezes the assumptions | "X doesn't scale" arguments with no load figures |
| Decision drivers | Makes the weighting explicit | The winner wins every row on random criteria |
| Options (2+) | Proves alternatives were considered | One option: an announcement, not a decision |
| Consequences incl. negatives | Prepares the team for the downsides | Surprise costs; loss of trust in ADRs |
| Revisit if… | Turns "forever" into a condition | Endless re-debates, or never revisiting |
docs/adr/
README.md # index: number, title, status
0001-record-architecture-decisions.md
0002-modular-monolith-with-worker-mode.md
0003-module-boundaries-with-ci-baseline.md
0004-booking-events-outbox.md
0005-payment-port-two-adapters.md
0006-photo-resizing-on-functions.md # explains the concurrency cap of 50
0007-ports-only-where-rules-are-rich.mdInteractive simulation
Visual diagram
How an ADR flows from a decision to the repo, and how it is later superseded.
Analogy
- A ship's logbookOne dated entry per course changeADRsOne ADR per decision
- A ship's logbookWeather, heading, reasonADRsContext, decision, consequences
- A ship's logbookNew entry, old one left untouchedADRsSuperseding, not editing
- A ship's logbookThe next captain reads the logADRsNew engineers understand past choices
- A ship's logbookNot a sailing manualADRsNot a design document
Where the analogy breaks: a ship's log records everything. ADRs only record the few decisions that are hard to reverse or likely to be questioned.
Check your understanding
- 1.Consequences and revisit triggers
- 2.Context (facts, numbers, forces)
- 3.Decision
- 4.Title, status, date
- 5.Considered options with pros and cons
- 6.Decision drivers
Practice ladder
Follow the supersede chain
Scenario Warm-upA year from now, StayHub's ADR index looks like this:
| # | Title | Status |
|---|---|---|
| 0004 | Publish booking events through an outbox (relay to workers) | Superseded by 0019 |
| 0006 | Photo resizing on functions, cap 50 | Accepted |
| 0012 | Search on PostgreSQL full-text | Superseded by 0021 |
| 0019 | Relay outbox events to managed Kafka | Accepted |
| 0021 | Hotel search on a search engine | Proposed |
Theo asks three questions. Answer each, and say which ADRs he should read: 1. "Do we still use an outbox?" 2. "Is search on a search engine now?" 3. "Can I raise the photo cap to 200?"
Finish ADR-0006
Explain PracticeYou've drafted ADR-0006 for photo resizing. Two sections are blank:
# ADR-0006: Resize hotel photos with functions, concurrency cap 50 Status: Accepted (2026-06-02) ## Context ~4,000 photos uploaded Sunday 19:00–21:00, ~300/day otherwise (~24,000/month). Resizing on web servers pushed search p95 from 0.4 s to 2.6 s every Sunday. PostgreSQL max_connections is 200; the monolith and workers use ~100. ## Options - Two extra always-on servers: ~$120/month, idle most of the week. - Worker processes: no new platform, still need Sunday capacity. - Functions triggered by storage events: ~$0.60/month compute, cold starts, new platform. ## Decision Functions, 1 DB connection per instance via a pooler, concurrency cap 50. ## Consequences ___ ## Revisit if ___
Write ADR-0002 for the modular monolith
Explain PracticeWrite ADR-0002 recording this quarter's first big decision: a modular monolith with worker mode instead of 12 microservices.
Facts from that week: 14 engineers, 3 teams, one on-call rotation, no platform team; features waited ~3 days for a release slot; a PDF memory leak crashed all 4 servers for 12 minutes; most features touch booking, pricing and payments together; 4 servers at 35% CPU.
Include all sections: status and date, context, drivers, at least three options, decision, consequences (good and bad), revisit if.
Has ADR-0004's trigger fired?
Decision StretchEight months later. ADR-0004 says: "Revisit if we extract a service that consumes booking events, or more than 6 consumers."
Facts now: Payments has been extracted as a service and needs booking events; consumers are email, loyalty, invoicing, channel sync, search counts, reviews, analytics (7); the analytics team wants to replay 3 months of events to backfill a new dashboard; the outbox relay works fine (oldest unsent row usually < 2 s).
Decide whether to supersede ADR-0004, and with what. Write it as: "We chose X over Y because…, accepting the risk of…", and say exactly what happens to ADR-0004's file.
Explain it yourself
Explain to a new team lead why the team writes ADRs, what a good one contains, and how decisions get changed later.