Data & System Architecture, from the ground up Lesson 4 / 80

Architectural Decision Records (ADRs)

Capturing decisions with their alternatives and consequences. The Michael Nygard format that most teams have converged on.

Six months from now, somebody on your team is going to point at a piece of the system and ask: “why does the auth service use JWT instead of sessions?” Or: “why are we on Postgres and not MongoDB?” Or: “why is the queue Redis and not RabbitMQ, when we already had RabbitMQ in another service?”

The honest answer, in most teams, is: nobody remembers. The person who made the call left in 2024. There was a long Slack thread, but it scrolled off the free tier. There was a meeting, but the notes are in someone’s private OneNote. There was a wiki page, but it was last edited two years ago and references a company that has since rebranded twice. The reasoning is gone. What remains is the decision, sitting in production, doing its job, with no one able to explain why it was the right call.

This is the problem ADRs solve. Architectural Decision Records are short, plain-text documents, written at the time of the decision, that capture what was decided, what the alternatives were, and what the consequences will be. They live in the repository, next to the code they describe. They are not deleted when they go out of date; they are marked superseded and the new ADR points back at the old one. Done well, ADRs become the archaeological record of your system: a future engineer can read them in order and understand how the architecture got to where it is.

This lesson is about the format that has won (Michael Nygard’s, from 2011), what to put in an ADR, when to write one, and what real public ADRs look like. It is short on theory and long on “here is what you actually do on Tuesday morning when a decision needs to be made.”

The Nygard format

In November 2011, Michael Nygard, the author of Release It! and a long-time consultant at companies that had been around long enough to regret their architectures, wrote a blog post titled Documenting Architecture Decisions. The post is fewer than 1,000 words. It proposed a format with five sections:

  1. Title. A short noun phrase. “Use Postgres for the order service.”
  2. Status. Proposed, accepted, deprecated, or superseded by ADR-0017.
  3. Context. What is the situation, what are the forces in play, why is a decision needed.
  4. Decision. What we are going to do, in active voice. “We will use Postgres.”
  5. Consequences. What becomes easier, what becomes harder, what we have committed ourselves to.

That’s it. The whole format is 30 lines of explanation in the original post. Almost every ADR template you see in the wild today is this format with cosmetic variations: someone added an “Alternatives Considered” section, someone else split Consequences into “Positive” and “Negative”, someone added a “Related decisions” footer. The bones are Nygard’s.

The format works because of what it deliberately leaves out. There is no executive summary. There is no risk matrix. There is no RACI chart. There is no roadmap. An ADR is not a project document; it is a decision document. It records one decision, and only one, in roughly one to two pages of plain Markdown. Anything bigger is a different artefact.

Status

Status is a single word or short phrase. The lifecycle is:

  • Proposed: someone has written the ADR, it is up for review, the team has not committed yet.
  • Accepted: the team has agreed; the decision is now in force.
  • Deprecated: the decision is no longer being followed, but no replacement exists yet.
  • Superseded by ADR-NNNN: a newer ADR replaces this one. The old ADR stays in the repo; you just update the status line and add a back-reference.

The crucial point is that ADRs are append-only. You do not delete superseded ADRs. You do not edit them in place to reflect the new decision. You write a new ADR, link the two, and leave the old one there as a record. Six months from now, when someone asks “wait, didn’t we use Postgres?”, they can read ADR-0007 (“Use Postgres for the order service”, Superseded by ADR-0034), then read ADR-0034 (“Move order service to DynamoDB”), and see the entire arc.

Context

The context section is where most of the value sits. It describes the world at the time the decision was made: what we were building, what the constraints were, what we had already committed to elsewhere, what we were afraid of. Future you will not remember any of this. Future you will see the production system as it stands and will have no idea what trade-off was being navigated.

Good context sections name the forces explicitly. “We expect roughly 50 writes per second and 500 reads per second in year one, scaling to maybe 10x that by year three. We have one engineer with deep Postgres experience and nobody who has run MongoDB in production. The compliance team requires that we be able to point at row-level audit logs.”

Decision

Active voice, present tense, one paragraph. “We will use Postgres 16, hosted on RDS, with a single primary and one read replica in another availability zone.”

Not “Postgres seems like a good choice.” Not “the team is leaning towards Postgres.” The decision section commits.

Consequences

Consequences are the honest section. Every decision closes some doors and opens others. Spelling out the negative consequences is not pessimism; it is the only way the next person can evaluate whether the trade-off still holds.

“We commit to running Postgres in production, which means our on-call engineers need to learn Postgres operational concerns: vacuum, replication lag, connection pooling. We give up the per-document flexibility of a document store; if we need to add fields frequently we will pay for migrations. We benefit from strong transactional guarantees and a query language the team already knows.”

Where ADRs live

ADRs go in the repository, next to the code they describe. The conventional path is docs/adr/ or docs/architecture/decisions/. Filenames are numbered: 0001-record-architecture-decisions.md, 0002-use-postgres-for-orders.md, 0003-jwt-for-auth.md. The first ADR is almost always the one that says “we are going to use ADRs,” which feels recursive but is genuinely useful: it commits the team to the practice and tells future contributors where to look.

Plain Markdown. No special tooling required. There is a small tool called adr-tools, a shell script that creates new ADRs from a template and manages the numbering, and there are language-specific equivalents in Node, Python, and Go. Use one if you like, or just copy the previous ADR file and increment the number. The format is dirt-cheap on purpose.

PRs that introduce significant architectural changes should include the ADR in the same commit. Reviewing the ADR is reviewing the design. If the team disagrees with the decision, the ADR gets revised before the code lands. This is one of the few documentation practices that actually keeps documentation in sync with the code, because the documentation is part of the code review.

When to write an ADR

The rule of thumb that works in practice: write an ADR for any decision that takes a Slack-thread argument to settle. If two reasonable engineers could disagree, and the team had to discuss it for more than 15 minutes, that decision is worth recording. If the answer is obvious to everyone, skip it.

Things that usually warrant an ADR:

  • Choice of database, queue, cache, or any infrastructure with a long replacement cycle.
  • Authentication and authorization model (sessions vs JWT vs OAuth flow choice).
  • Service boundary decisions: “this is one service” vs “this is two services”.
  • Public API contracts: REST vs gRPC vs GraphQL.
  • Cross-cutting concerns: logging format, tracing strategy, error-handling conventions.
  • Build and deploy decisions: monorepo vs polyrepo, CI provider, container registry.

Things that don’t:

  • Variable names, file layout within a service, formatting choices.
  • Small library choices that are easy to swap (which JSON parser, which date library).
  • Anything captured by a linter or a code review.

The risk on a young team is writing too many; the risk on a mature team is writing too few. Both fail the same way: nobody reads them anymore. Aim for somewhere between five and twenty ADRs in the first year of a project, growing slowly after that.

Real public ADRs

Several large projects publish their ADRs publicly. They are worth reading both as examples of the format and as windows into how serious teams reason about trade-offs.

Backstage, Spotify’s open-source developer-portal platform, has over 30 public ADRs covering decisions like “use Lerna for monorepo management” (later superseded), “expose the React component for breadcrumbs”, and “version-control plugin metadata in YAML.” The format is exactly Nygard’s, with light additions. Read ADR-001 (“Architecture Decisions”) and ADR-005 (“Catalog Core Entities”) to see the house style.

Arc42 maintains a community website with a large catalogue of ADR examples and templates, including alternatives to the Nygard format (the MADR format adds an “Alternatives Considered” section as a first-class block, which I would recommend). It is the closest thing the ADR community has to a canonical reference.

The ThoughtWorks Technology Radar is not an ADR repository, but it is the closest large-scale public document of “things our practitioners think you should adopt, trial, assess, or hold.” It is essentially the industry’s collective ADR feed for tooling decisions, and it is updated twice a year.

If you only read one external ADR before writing your first, read Backstage’s ADR-001. It is short, it is the project committing to keep ADRs, and it shows the format used in anger.

A worked example

Here is what an ADR looks like for a fictional team that has just decided to use Postgres for their order service. This is the entire file; there is no other documentation of the decision.

# ADR-0007: Use Postgres for the order service

## Status

Accepted, 2025-10-28.

## Context

The order service stores customer orders, line items, payments, and
fulfilment status. Expected load in year one is ~50 writes/sec and
~500 reads/sec, growing to roughly 10x that by year three. The data
is highly relational: orders have many line items, line items
reference products and pricing, payments reference orders and
refund chains.

The team has three engineers with deep Postgres experience and
nobody with MongoDB in production. Our finance partners require
row-level audit logs and ACID guarantees on payment state
transitions. The platform already runs Postgres for the user
service, so we have a managed RDS pattern, backup tooling, and
on-call runbooks for it.

We considered Postgres, MongoDB, and DynamoDB.

## Decision

We will use Postgres 16 on AWS RDS for the order service. One
primary in eu-central-1a, one read replica in eu-central-1b for
analytics queries and failover. PgBouncer in front for connection
pooling. Schema migrations via Flyway, run on deploy.

## Consequences

Positive:
- Strong transactional guarantees on payment state changes.
- Reuse of existing operational tooling, backups, alerts.
- Team can debug query plans on day one.
- SQL is a known quantity for analytics and ad-hoc queries.

Negative:
- We commit to schema migrations for every new field. We will
  pay this cost on every release.
- Single-region primary; cross-region writes are not supported.
  If we ever need EU + US write locality, we will need to revisit.
- We are now responsible for index design. A bad query at 10x
  load will hurt.

Alternatives considered:
- MongoDB: rejected. Team has no operational experience, and the
  schema flexibility benefit is not worth the audit-log cost.
- DynamoDB: rejected. Single-table design would constrain the
  analytics team, and we would still need a separate relational
  store for reporting.

That is the entire artefact. One markdown file, in docs/adr/0007-use-postgres-for-orders.md, committed alongside the first migration that creates the order tables. Two years from now, when someone asks why orders are on Postgres, this is the answer.

The lifecycle

Here is the ADR lifecycle as a flowchart:

flowchart LR
    A[Proposed] --> B[Accepted]
    A --> X[Rejected]
    B --> C[Deprecated]
    B --> D[Superseded by ADR-NNNN]
    C --> D

Most ADRs go from Proposed to Accepted in a single PR review and then sit in Accepted forever, doing their job. A small number get superseded as the system grows up. A handful get deprecated without replacement, usually because the thing they decided about no longer exists.

The graveyard of superseded ADRs is the part most teams find unintuitive. The instinct is to delete the old one because it is “wrong now.” Resist the instinct. The old ADR is the historical record of why the team thought differently in 2024, and the chain of supersessions is a map of how the architecture evolved. When a new engineer asks “why are we like this?”, that chain is the answer.

What ADRs are not

ADRs are not specifications. They do not describe in detail how the order service is built; they describe one decision about it. The full design lives in code, in API docs, in runbooks. The ADR captures only the choice that mattered at the time.

ADRs are not contracts. The team is allowed to change its mind, and supersedes a previous ADR with a new one. The lightness of the format is what makes this practical: rewriting a 50-page design doc when the database changes is unthinkable, but writing ADR-0034 that supersedes ADR-0007 is a Tuesday afternoon’s work.

ADRs are not project plans. They do not have schedules, owners, or budgets. If your ADRs are growing those sections, you are slowly turning them into something else, and the format will stop working.

Starting tomorrow

Here is the starter kit for a team that has never written an ADR:

  1. Create docs/adr/ in your main repo.
  2. Copy the Nygard template into 0001-record-architecture-decisions.md and adapt it: “we will record architectural decisions using ADRs in the Nygard format.”
  3. The next time a real architectural argument breaks out on Slack, write 0002-<whatever-it-was>.md and link it in the thread.
  4. Add a checkbox to your PR template: “If this PR makes an architectural decision, is there an ADR?”

That is the entire onboarding. The format is small enough that a team adopts it in an afternoon, and after a year of use the decision archive becomes one of the most-read documents in the repository.

Next lesson, we move from “how to record decisions” to “why every decision is a trade-off, and which ones come up most often in this craft.”

Search