Skip to content

Writing ADRs

A technical decision recorded in writing, with the alternatives that were considered and rejected. The artefact that survives the developer leaving. Written before the code, not after.

TL;DR

An ADR captures one constrained choice that would cost more than a day to undo. It uses MADR format — seven sections. It has at least two real options (the third is usually do nothing). It is signed by two engineers before the code lands. If you cannot name a real alternative, the decision is not an ADR — it is a default.

What it is

An ADR (Architecture Decision Record) records a technical decision that constrains future code. It is named in What We Shape · ADR. It does not record decisions with no constraints — we are using TypeScript is not an ADR. We are using Mongoose rather than Prisma for this service because of [reason], accepting [trade-off] is.

Distinguish from

Technical Design Brief — the cycle's technical shape; references ADRs. System design doc — multi-cycle, multi-team; ADRs are one decision. RFC — proposal stage; ADR is the signed decision. See Confusable with at the foot.

Why it matters

Without ADRs:

  • Choices are made implicitly. The only record is in commit history; the why is lost within six weeks.
  • The same choice gets re-made. Three engineers each fix the same symptom differently because nobody agreed once.
  • Reversibility decays. Reversing a never-recorded choice costs more than reversing a signed one.
  • The next senior engineer cannot reason from precedent. They re-invent or worse, re-litigate.

The ADR is the corpus's discipline against the architecture happening by accident.

How to do it

Step 1 — Decide whether it deserves an ADR

Two filters. If the answer is yes to both, write an ADR.

  • Would undoing this choice take more than one day of work?
  • Will a reader six months from now want to know why this rather than the alternative?

Filing every decision as an ADR floods the record. Filing too few hides the structural choices. The rule of thumb: a normal cycle produces 0–2 ADRs. A foundational cycle (new service, new persistence, new auth) may produce 3–5.

Step 2 — Status: Proposed

Open the ADR as Proposed. The Proposed state is for the team to read and react. Do not start coding the chosen option while the ADR is Proposed.

text
Status: Proposed
Date:   2026-05-17
Author: the TL

Step 3 — Context and Problem Statement

What forced this decision now? Bind it to the cycle's brief.

text
## Context and Problem Statement
The cycle's Feature Brief commits to "Gal grades a cycle in
<15 min". The cycle requires persisting normalised Hebrew
names with diacritic-fold lookup. The current Mongoose
service does not support custom collation per field.
We must decide where the normalised form lives and how
it is queried.

Step 4 — Decision Drivers

The forces and constraints. Two to five, no more.

text
## Decision Drivers
- Read-path performance: queue render <300 ms (TDB ility).
- Reversibility: this cycle's brief, with possible follow-up
  cycles changing the locale set.
- Operational cost: existing team's familiarity with current
  ORM.
- Schema migration cost: ~50 M rows in submissions table.

Step 5 — Considered Options (≥2, including do nothing)

At least two real options. Do nothing is always one of them. Do nothing means: ship the cycle without solving this, with named consequences.

text
## Considered Options
1. Application-side normalisation on write, indexed normalised
   column in DB.
2. DB-side trigger normalising on insert/update, with
   covering index.
3. Do nothing — accept current alt-tab pattern, write a story
   for a documented Hebrew-name search workaround.

If you cannot name a second option, the "decision" is a default. Stop. Find the second option, or downgrade this to a TDB note.

Step 6 — Options Analysis

Pros and cons for each, anchored in the drivers.

text
## Options Analysis

### Option 1 — Application-side normalisation
Pros:
  - Lives in our codebase; team owns it.
  - Easy to evolve when locale set changes.
  - Testable in unit tests.
Cons:
  - Read-only queries from other services bypass it.
  - 50 M-row backfill needed.

### Option 2 — DB-side trigger
Pros:
  - All paths through DB get correctness.
  - No application code touches normalisation.
Cons:
  - DB-side logic harder to evolve; harder to test.
  - Team less familiar with trigger debugging.
  - Same 50 M-row backfill needed.

### Option 3 — Do nothing
Pros:
  - Cycle ships faster.
  - No 50 M-row backfill risk.
Cons:
  - The brief's prediction will not be met.
  - The alt-tab workaround remains a Level 2 chain failure
    we are knowingly accepting.

Step 7 — Decision Outcome

The chosen option, with the trade-off explicitly accepted.

text
## Decision Outcome
Chosen: Option 1 — Application-side normalisation.

Rationale:
  Reversibility is the dominant driver. The locale set will
  change in the next two cycles; application-side normalisation
  is changeable in code. The 50 M-row backfill is identical for
  options 1 and 2; option 1 only adds a wrapper in the service
  layer.

Trade-offs explicitly accepted:
  - Read-only queries from other services that query the
    submissions table directly will see un-normalised names
    until a follow-up ADR migrates them. ETA: 1 cycle.
  - The team takes on the operational responsibility of
    keeping the locale map current.

Step 8 — Consequences

Positive, negative, and risks.

text
## Consequences
Positive:
  - Read-path performance protected by indexed normalised
    column.
  - Easy to add Russian/Spanish locales without DB change.

Negative:
  - One additional service-layer write path to maintain.

Risks:
  - If the locale map drifts between application and the
    backfill, queries return stale data.
    Mitigation: locale map version pinned in DB row metadata.

Step 9 — Sign and move to Accepted

Two engineers sign. Status moves to Accepted. The ADR is now precedent — future ADRs that contradict this must explicitly say so (Superseded by ADR-NNN).

text
Signed by: the TL, the senior dev
Status:    Accepted
Date:      2026-05-19

A complete ADR

See the template for the copy-paste skeleton.

Evidence

Across cycles, ADRs that aged well shared three properties.

  1. They named do nothing as a real option. ADRs without a do nothing option were superseded within two cycles 3× more often than ADRs with do nothing. The discipline of writing do nothing honestly often changed the chosen option.
  2. The trade-off was named in the Decision Outcome, not buried in Consequences. ADRs whose trade-off lived in Decision Outcome · Trade-offs explicitly accepted produced less drift in the implementation than ADRs whose trade-offs lived in Consequences.
  3. They were signed before the code. ADRs written after the code landed described what was rather than why we chose; the why decayed within a quarter.

Anti-patterns

PatternWhat it looks likeWhere to fix
One-option ADR"We considered approach X and went with X"Clinic — An ADR with one option
ADR written after codeThe choice is in commit history; ADR is retroactiveWrite at Proposed before code; the team reads it before approving
Trade-off buried in Consequences"Things we lost" listed but never confrontedMove to Decision Outcome · Trade-offs explicitly accepted
Default decisions filed as ADRsWe chose REST because… (no real alternative considered)This is not an ADR; record it in the TDB instead
ADRs that never get supersededStatus all Accepted, none SupersededEither the architecture never changes (unlikely) or supersessions aren't being recorded — the precedent is decaying silently

Confusable with

ThisNot thisDifference
ADRTDBTDB = the cycle's technical shape; ADR = one constrained choice that survives the cycle
ADRRFCRFC = proposal for discussion; ADR = signed decision
Do nothingDeferDo nothing is we ship without it, accepting [consequence]; defer is we'll do it next cycle
Trade-off acceptedConsequenceTrade-off = what we knowingly chose to lose; consequence = what happens next

Further reading

200apps · How We Work · NWIRE