Skip to content
Episode 35 · Why is the outbox here?

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.

30 min intermediateArchitecture decision recordsADR lifecycle (proposed, accepted, superseded)Problem → requirements → constraints → options → trade-offs → decisionTrade-off thinking
  1. 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.

  2. 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.

  3. Engineering concept

    Record each significant decision as a short, immutable document: context, drivers, options, decision, consequences. Change decisions by superseding, never by rewriting history.

  4. 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.

  5. Trade-offs

    A little writing effort per decision versus repeated debates, onboarding cost and accidental reversals. Over-documenting trivial choices wastes that effort.

01

The problem

2 min
Monday, 10:30. Theo, in his second week, opens a pull request.
  1. Theo
    PR #2214: remove the outbox table. It looks like unused complexity.
  2. Omar · On-call SRE
    That's how we lost 9 bookings in the spring. Two hotels resold rooms.
  3. Theo
    How was I supposed to know? I searched the docs.
  4. Lina · Senior engineer, your mentor
    There's nothing to find. Last episode of the module: you lead this one.
02

The big idea

4 min
Decisions that only live in chat
The big idea

The 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.

03

How it works

5 min

An 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.

The life of an ADR
  1. 1
    Spot the decision

    Expensive to reverse, crosses teams, or likely to be questioned? Write one. A date library? Don't.

  2. 2
    Draft 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".

  3. 3
    Review in a pull request

    Affected teams challenge numbers and add options before any code is written.

  4. 4
    Accept and index

    Status moves Proposed → Accepted, with a date; listed in docs/adr/README.md.

  5. 5
    Supersede, never edit

    When a revisit trigger fires, a new ADR replaces it. The old one stays, marked with a link.

04

Watch a decision being made

4 min
Worked example

You 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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

The whole thing in one line: Worth recording, context with numbers, explicit drivers, honest options, costs stated, a measurable revisit trigger, linked from the code.
05

In real life

3 min
Short records, or long narratives

Spotify: 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.

06

What goes wrong

3 min
Editing an accepted ADR to change the decision

The history of reasoning disappears.

Instead

A new ADR that supersedes it; change only the old status line.

Strawman options, or only positive consequences

It reads like a sales pitch, and readers stop trusting ADRs.

Instead

Real alternatives with honest pros; list what gets harder.

An ADR for every small choice

Hundreds of trivial records bury the important ones.

Instead

Only decisions that are costly to reverse, cross teams or will be questioned.

07

Trade-offs

3 min
ApproachFits whenYou pay
Lightweight ADRs in the repoAlmost always beyond a handful of engineersA habit someone must nudge
Heavy design docs / review boardsMulti-quarter, multi-team or compliance decisionsSlow; teams route around them
Nothing writtenThrowaway prototypesRe-debates and accidental reversals
Tuesday, 15:00. How strict to be about ADRs.
  1. Omar · On-call SRE
    Make it mandatory: CI fails any PR touching infra/ or a module API without an ADR link.
  2. Karim · CTO
    That's 20 PRs a week. People will write empty ADRs to get green builds. Keep it a habit.
  3. Omar · On-call SRE
    Habits are how we ended up with zero ADRs for six decisions.
08

Your job as tech lead

4 min

Claude 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.

Decisions you own
  • 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.md to read docs/adr/ before changing anything an ADR covers.
Tell Claude Code
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.
Check before you accept
  • 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.
Red flags
  • 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/.
09

Check yourself

2 min
Q1/3Scenario
ADR-0012 chose PostgreSQL full-text search for hotel search. Two years later you move search to a dedicated search engine. What do you do with ADR-0012?
How confident are you?

Next 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.

Finished?

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.
10

Technical deep dive

Writing ADR-0004 together

You write the retroactive ADR for the outbox; Lina only reviews. Here's the method, then the finished record.

Worked example

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.

  1. 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.

  2. 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.

  3. 3Name the drivers
    1. 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.

  4. 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.

  5. 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.

  6. 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.

The whole thing in one line: You checked the decision was worth recording, wrote the context with numbers, named three drivers, compared three honest options, stated the decision with its costs, and added a revisit trigger linked from the code.
markdown
# 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.
docs/adr/0004-booking-events-outbox.md (the finished record)
SectionPurposeSmell when missing or weak
Status + dateTells readers whether it still appliesPeople act on decisions superseded years ago
Context with numbersFreezes the assumptions"X doesn't scale" arguments with no load figures
Decision driversMakes the weighting explicitThe winner wins every row on random criteria
Options (2+)Proves alternatives were consideredOne option: an announcement, not a decision
Consequences incl. negativesPrepares the team for the downsidesSurprise costs; loss of trust in ADRs
Revisit if…Turns "forever" into a conditionEndless re-debates, or never revisiting
Each section answers a question a future reader will ask.
bash
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.md
StayHub's ADR log after this episode.
11

Interactive simulation

12

Visual diagram

Click a component

How an ADR flows from a decision to the repo, and how it is later superseded.

13

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.

14

Check your understanding

Q1/3Recall
Order the sections of a typical ADR:
  1. 1.Consequences and revisit triggers
  2. 2.Context (facts, numbers, forces)
  3. 3.Decision
  4. 4.Title, status, date
  5. 5.Considered options with pros and cons
  6. 6.Decision drivers
How confident are you?
15

Practice ladder

Step 1 · Trace

Follow the supersede chain

Scenario Warm-up

A year from now, StayHub's ADR index looks like this:

#TitleStatus
0004Publish booking events through an outbox (relay to workers)Superseded by 0019
0006Photo resizing on functions, cap 50Accepted
0012Search on PostgreSQL full-textSuperseded by 0021
0019Relay outbox events to managed KafkaAccepted
0021Hotel search on a search engineProposed

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?"

Step 2 · Complete

Finish ADR-0006

Explain Practice

You'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
___
Step 3 · Solve

Write ADR-0002 for the modular monolith

Explain Practice

Write 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.

Step 4 · Decide

Has ADR-0004's trigger fired?

Decision Stretch

Eight 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.

16

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.

0 words