Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Architecture Decision Records

An architecture decision record (ADR) holds the why behind a design: the problem it answers, the choice made, and what that choice rules out. The rest of the documentation says what the server does. The module docs (//!) say what each piece of code guarantees. The ADRs are where the argument lives, so that the argument is not restated wherever the decision is mentioned.

Read the relevant ADR before changing something it covers. If a change reverses a decision, write a new ADR that supersedes the old one and change the old one’s status. Do not rewrite its argument after the fact.

ADRDecisionStatus
0001Before 1.0.0, only the database schema is a compatibility promiseAccepted
0002One binary over a layered workspace of lockstep cratesAccepted
0003Migrations are append-only and applied only by the schema ownersAccepted
0004Row ids are UUID v7 stored as BLOBs, and their type says where they came fromAccepted
0005A Rust enum owns each vocabulary, and SQL checks only the closed onesAccepted
0006A request does no slow or privileged work; it queues itAccepted
0007One binary runs as role processes, and only the worker holds the CA keyAccepted
0008State that more than one process can see lives in the databaseAccepted. One exception stands, the web admin’s login limiter, for as long as
0009Dependencies are pure Rust on ring, add no global state, and earn their placeAccepted; metrics clause superseded by 0011, crypto clause narrowed by 0014
0010Errors derive thiserror, carry their whole message, and panic only at startupAccepted. This was re-argued more than once before it was written down, which
0011Metrics are built on prometheus-client, one registry per scrapeAccepted
0012Images are built natively per architecture, uncached, and published only past a guardAccepted
0013main is the trunk, and a patch line is a release branch cut when a fix needs oneAccepted
0014PostgreSQL is chosen by the URL’s scheme, over one set of queriesAccepted

Decisions argued elsewhere

Some decisions are explained where their subject lives, and have no record here, because a second copy would drift from the first.

In the book:

In a module’s own documentation (//!), where the decision concerns that module alone:

  • crates/jobs/src/jobs/mod.rs: why background work is a durable queue and not a tokio::spawn, and the Retry/Failed split every handler must honour.
  • crates/signer/src/local_ca/crl.rs: how the CRL number stays monotonic across processes.
  • crates/policy/src/filter/policy.rs: Kleene logic, and why a rule’s stages are an intersection.
  • crates/policy/src/ipam/mod.rs: why an inventory never denies, so an outage cannot fail open.
  • crates/core/src/script_hook.rs: the one contract every custom script runs under.
  • crates/core/src/templating.rs: .html escapes and .j2 does not, decided by the name.
  • crates/jobs/src/metrics.rs: bounded cardinality, families declared even when empty, and counters that survive a reload.
  • crates/jobs/src/notify/expiry.rs: why the expiry notice is a digest.
  • crates/server/src/reload.rs: the swap’s mechanics and what stays frozen.

Writing an ADR

Name the file NNNN-kebab-title.md, taking the next free number, and list it both in the table above and in SUMMARY.md under this page. doc/lint.py refuses an ADR that is missing from either list, or that lacks one of the five sections below.

The title is # ADR NNNN: <decision>, and the sections come in this order:

  • ## Status: Accepted, or Superseded by ADR NNNN. Name a proposal to replace it if one is on record.
  • ## Context: the problem, and the history that makes the rule necessary. History with no rule behind it does not belong here — git and CHANGELOG.md already keep it.
  • ## Decision: what was chosen, stated as rules.
  • ## Consequences: what the decision costs and what it rules out.
  • ## Enforced by: the tests, startup refusals and type-level constructs that hold the decision in place, named so that a grep finds them. Write “Review only” when nothing does.

Link to the page that owns a configuration key rather than restating its default, since the book documents each key in exactly one file.