Writing ADRs #
An architecture decision record says what we decided, why, and what it costs. One record holds one decision.
Records live in docs/adr, numbered in the order they were made.
The point is not process. It is that six months from now the code will not tell you why a color space carries its own context, and neither will the person who wrote it. The record will.
When to write one #
Write a record when a choice is hard to reverse and shapes code nobody has written yet. In practice that means:
- A new module boundary or a trait other code will implement.
- A change to the public API that crate users will build on.
- A new feature flag scheme, or a dependency that reaches through the crate.
- Picking one of several workable approaches for a reason that is not obvious from the result.
Do not write one for a dependency bump, a rename, a bug fix, or anything a comment on the code covers. A record you have to write to satisfy a rule is a record nobody will read.
If you cannot name the alternative you rejected, you probably do not have a decision. You have an implementation.
When to write it #
Write the record before the code, in its own commit, sitting directly under the commits that carry it out. The log then reads as the decision followed by the work, and a reviewer can argue with the reasoning before reading the diff.
Status #
A record is active the day it lands. We do not commit decisions we have not made, so there is no status for a
record still under discussion. That argument happens in an RFC, before the commit.
| Status | Badge | Meaning |
|---|---|---|
active |
Decided. The code follows it. | |
deprecated |
We stopped doing this and nothing replaced it. | |
superseded |
A later record replaced it. Its number goes in the badge. |
The status appears twice: in the front matter, where you can search it, and as a badge under the title, where you can see it. Keep the two in step.
A record is not edited once it lands, apart from its status. If the decision changes, write a new record and mark the old one superseded. The wrong turns are worth keeping. They are half of why the current shape is what it is.
Superseding takes three edits to the old record: the status, the superseded-by key, and the badge, whose
XXXX becomes the number of the record that replaced it. The new record lists the old one under supersedes.
Front matter #
Every record opens with front matter. Leave out any key you have nothing to put in.
| Key | Holds |
|---|---|
id |
The record's number, quoted, so the padding survives YAML. |
title |
The title from the heading, without the ADR NNNN: part. |
status |
active, deprecated or superseded. |
created |
The day the record landed. It never changes, whatever later happens to the record. |
area |
The parts of the crate the decision lands in. Use the commit scopes. |
supersedes |
The numbers of the records this one replaces. |
superseded-by |
The number of the record that replaced this one. |
tags |
Anything worth searching on later. |
Records 0001 to 0007 came before this template. They keep their own shape, since a record is not edited once it lands.
How to write one #
Number the file with the next free number, four digits, then a short slug: 0008-cache-the-matrix-inverse.md. A
number is never reused, and a gap is never filled.
Title the record with the decision, not the topic. "Store components as f64" tells a reader what happened. "Component precision" does not.
Write in the present tense and the active voice, as the team: "We store components as f64". The writing rules apply here as they do everywhere else.
Say what it costs. A record with no consequences worth naming is either a decision that did not matter or a record that is not finished. Include the bad ones. The record earns its keep the day somebody hits a cost we already knew about and finds it written down.
Keep it to a page. Cut every sentence that a reader could have worked out from the section above it.
Then add a row to the index.
The template #
---
id: "<ID>"
title: ADR Title
status: active
created: YYYY-MM-DD
area: []
supersedes: []
superseded-by:
tags: []
---
# ADR NNNN: The decision, stated as a decision
![Active][status]
## Context
The forces in play. What the code looked like, what we needed it to do, and the constraints that ruled anything
out. Enough that a reader who was not there can reach the same decision.
## Decision
What we decided, in the present tense and the active voice. Name the parts of the crate it lands in.
## Alternatives
The options we turned down and the reason each one lost. Leave the section out if there were none worth naming.
## Consequences
What follows from this, good and bad. What gets easier, what gets harder, and what we will have to live with.
[status]: https://img.shields.io/badge/Active-green?style=for-the-badge