Writing ADRs
On this page
When to write an ADR
Write an ADR when the decision changes how future contributors will approach the codebase. The bar is judgment, not rules — but past ADRs give the calibration:
-
ADR-001 chose Rust for the monorepo. Without this record, every new contributor would re-litigate the language choice.
-
ADR-013 picked encrypted cookies over server-side sessions. The mechanism itself is short; the reasoning (stateless horizontal scaling, no Redis dependency) is what future readers need.
-
ADR-019 introduced the
metadatablock extension for JDM rulesets. The mechanism is small; the architectural pattern (decision policy as static config, not per-request evaluation) is what generalizes.
If a decision is reversible in the next MR with no broader cost, it doesn’t need an ADR. If reversing it would mean refactoring multiple services or migrating data, it does.
ADRs are CRAIG’s RFC mechanism
Other projects separate "RFC" (proposal phase) from "ADR" (decision phase). CRAIG does not — the two are folded into a single document because separating them creates drift. An ADR can be filed in Proposed status, edited during discussion, then status-flipped to Accepted (or Mooted / Rejected) when the decision is final. See ADR-016 for an example of a Mooted ADR (the original event-driven convert_report design, superseded by ADR-017).
This is not a policy you can opt out of by filing "RFCs" in another forum — design discussions live as draft ADRs in this repo, full stop.
Structure
Existing ADRs follow this skeleton (numbered XX in docs/modules/ROOT/pages/adrs/adr-NNN-<slug>.adoc):
= ADR-NNN: Short Decision Title
:status: Proposed | Accepted | Mooted | Rejected | Superseded
:date: YYYY-MM-DD
== Status
{status} on {date}.
== Context
What problem are we solving? What are the constraints? What did we look
at and rule out?
== Decision
What is the decision in plain language? One paragraph, declarative.
== Consequences
What does this decision unblock? What does it lock us out of? What
follow-on work does it imply?
== Open questions
If `Proposed`, the questions blocking acceptance. If `Accepted`, the
questions deferred for follow-on work (with resolution dates).
== Alternatives considered
Concrete alternatives evaluated and rejected, each with the reason for
rejection.
The headers are mostly stable; the prose is short. The longest accepted ADR (ADR-019, ~280 lines) is unusual — most are 80–150 lines.
Numbering
Sequential. Take the next free number; do not reserve numbers in advance. Mooted/rejected ADRs keep their number — the numbering is historical, not canonical. ADR-016 stays ADR-016 even though it’s superseded by ADR-017.
If two ADRs are filed concurrently and collide, the later MR rebases on main and bumps the number. Don’t gap-fill.
Cross-references
ADRs cite each other freely. Use ADR-NNN: Title for canonical links. The ADR index in nav.adoc is the front door.
When an ADR supersedes another, the superseded ADR’s Status flips to Superseded by ADR-NNN, and the new ADR’s Context cites the prior one explicitly. ADR-016 → ADR-017 is the working example.
Decision log location
The full decision log is docs/modules/ROOT/pages/adrs/. The nav.adoc ADRs section is the index, ordered by ADR number. Mooted/superseded ADRs stay listed with their final status — readers learn from the rejected paths, not just the accepted ones.
Workflow
-
Draft: branch
docs/adr-NNN-<slug>, file the ADR with:status: Proposed. Open a draft MR. -
Discussion: comment on the MR. Edit the ADR in place; keep the MR draft until the decision is firm.
-
Accept (or
Mooted/Rejected): flip:status:, mark the date, un-draft the MR. Get one approval, merge. -
Implementation plan: if the ADR implies multi-step work, file a plan in
docs/modules/ROOT/pages/plans/<slug>.adocand link to it from the ADR’s Consequences. ADR-017’s plan (stateless-intake.adoc) and ADR-019’s plan (report-person-linking.adoc) are the two recent worked examples. -
Errata: if the implementation deviates from the ADR’s spec for a defensible reason, document the deviation in the plan (not the ADR) under an
== Erratasection. The plan tracks how the implementation actually shipped; the ADR records what was decided. ADR-019 plan errata E-01 (Step 6 sequential override) is the precedent.
What ADRs do not cover
-
API contracts — those live in
docs/modules/ROOT/pages/api/<service>.adocand the#[utoipa::path]macros that generate them. -
Schema decisions for a single table — usually too narrow. If the schema decision implies a cross-service pattern (e.g., ADR-002 UUID v7 for all PKs), it’s an ADR.
-
Implementation steps — those live in plans, not ADRs.
-
Bug-fix rationale — the commit message + CHANGELOG entry are sufficient.
Common pitfalls
-
Writing ADRs after the fact. ADRs document decisions; if the decision is already in main, the ADR is harder to write because the alternatives have been pruned. Better: file a draft ADR first, even a rough one. ADR-018 is a worked example of "we made the call, then wrote it up cleanly within the same plan-step MR" — but the cleaner path is draft-first.
-
One-paragraph context, three-paragraph decision. Inverse it. The context (constraints, options ruled out) is what readers need most; the decision itself is usually short.
-
Skipping the alternatives. Even if the decision was obvious, write down why the obvious option won. Future readers re-evaluate against new constraints; they can’t do that without knowing what was on the original short list.