How to Write Architecture Decision Records That Actually Get Used

The template, where to store them, and how to make the habit stick

Black-and-white illustration of a tombstone with the letters ADR inscribed on it, evoking Architecture Decision Records that sit unused.

The Context Nobody Writes Down

A few months ago, an engineer on my team wanted to change how we handled session management. Under normal circumstances, that's a week of investigation, a few conversations, a proposal, and then implementation.

Instead, the engineer pulled up an ADR we'd written eight months earlier. It laid out what we'd considered, why we chose the approach we chose, and what constraints would need to change before a different approach made sense.

The engineer read it, realized the original constraints still held, and saved the team two weeks of work on a migration that didn't need to happen.

That's what ADRs are for. Not documentation theater. Not compliance artifacts. A system that prevents your team from relitigating decisions that were already made with full context.

I wrote about why this matters from a leadership perspective … the bus factor problem, the knowledge that walks out the door when someone leaves. This post is the tactical companion. The template, the process, the automation, and the mistakes I made implementing ADRs across distributed teams in three countries.

What an ADR Actually Is

An Architecture Decision Record is a short document that captures one decision. One. The decision, the context around it, the options you considered, and why you chose what you chose.

If your ADR takes more than 30 minutes to write, you're overengineering it.

The Template

# ADR-{NUMBER}: {TITLE}
 
## Status
 
{Proposed | Accepted | Deprecated | Superseded by ADR-XXX}
 
## Date
 
{YYYY-MM-DD}
 
## Context
 
What is the situation? What forces are at play? What constraints exist?
Write this for someone who has zero context on the problem. Because in
18 months, that someone is you.
 
## Decision
 
What did we decide? Be specific. Name the technology, the pattern, the
approach. Don't hedge.
 
## Consequences
 
What becomes easier? What becomes harder? What are we explicitly accepting
as tradeoffs? What doors does this close?
 
## Options Considered
 
### Option A: {Name}
- How it works
- Pros
- Cons
- Why we didn't choose it
 
### Option B: {Name}
- How it works
- Pros
- Cons
- Why we didn't choose it
 
### Option C: {Chosen} ✓
- How it works
- Pros
- Cons
- Why we chose it

Status is first

When someone lands on an ADR, the first thing they need to know is whether this decision is still active. Deprecated and Superseded save people from following outdated guidance.

Context is written for strangers

You know why you're making this decision right now. The person reading this in 18 months does not. Every acronym, every assumption, every "obvious" constraint … write it down. The context section is where most ADRs fail because the author assumes shared knowledge that evaporates over time.

Consequences include the downsides

If your ADR only lists positive consequences, you didn't write an ADR. You wrote a press release. Every decision has tradeoffs. Name them. Future engineers will respect the honesty and use it to evaluate whether the tradeoffs still hold.

Options Considered is the most valuable section

This is what saves the two weeks. When someone wants to change the decision, they don't start from scratch. They read what was already evaluated and why it was rejected. Maybe the constraints changed and Option A is now viable. Maybe they didn't and the current decision still holds. Either way, they're building on prior thinking instead of repeating it.

A Real Example

# ADR-007: API Gateway Pattern for Composable Storefront
 
## Status
 
Accepted
 
## Date
 
2024-03-15
 
## Context
 
We are migrating from a monolithic storefront to a composable architecture.
The new frontend (Next.js) needs to communicate with five backend services
... product catalog, inventory, pricing, cart, and checkout. Each service
has its own API contract and authentication mechanism.
 
The frontend team is three engineers across two time zones. They've expressed
concern about managing five different API integrations directly from the
client. The current monolith handles all of this through a single BFF
(backend-for-frontend) layer that the team built two years ago.
 
Performance budget is 200ms for initial product page data. Current p95 is
340ms due to serial API calls in the monolith's BFF.
 
## Decision
 
We will use a lightweight API gateway (Express.js) deployed as a single
service that aggregates calls to backend services and exposes a simplified
API to the frontend.
 
The gateway will handle authentication, request aggregation, and response
shaping. It will NOT contain business logic. Business logic stays in the
individual services.
 
## Consequences
 
**Easier:**
- Frontend team manages one API contract instead of five
- Response shaping happens in one place
- Authentication is centralized
- We can parallelize backend calls in the gateway, directly addressing
  the 340ms p95
 
**Harder:**
- One more service to deploy and monitor
- Gateway becomes a potential single point of failure
- Risk of business logic creeping into the gateway over time
 
**Tradeoffs we're accepting:**
- Added operational complexity of one more service
- Team discipline required to keep business logic out of the gateway
- Gateway team needs to be responsive to downstream API contract changes
 
## Options Considered
 
### Option A: Direct Client-to-Service Communication
Each frontend component calls its respective backend service directly.
 
- Eliminates the gateway as a dependency
- Frontend team must manage five API integrations, auth patterns, and
  error handling approaches
- No request aggregation ... product page requires five round trips
- Rejected because the frontend team is too small to absorb this
  complexity and the performance budget is impossible without aggregation
 
### Option B: GraphQL Federation
Apollo Federation layer that composes a unified graph from all five services.
 
- Elegant query interface for the frontend
- Strong typing and schema validation
- Significant operational complexity (schema registry, gateway, subgraphs)
- Learning curve for the team (nobody has production GraphQL experience)
- Rejected because the operational overhead and learning curve don't
  justify the benefits for our current team size and timeline
 
### Option C: Express.js API Gateway ✓
Lightweight Node.js gateway that aggregates and shapes responses.
 
- Team already has deep Node.js experience
- Simple to build, test, and deploy
- Parallel request execution solves the performance issue
- Low operational overhead
- Chosen because it solves the core problems (aggregation, auth, response
  shaping) with the lowest complexity and fastest time to production

Notice what this ADR does that a Slack thread or a meeting never could.

Six months from now, when an engineer wants to propose GraphQL, they'll find this ADR. They'll see that GraphQL was already evaluated. They'll see why it was rejected. And they'll either agree that the constraints still hold or they'll make the case that constraints have changed (maybe the team grew, maybe someone with GraphQL experience joined). Either way, the conversation starts at a higher altitude.

Where to Store Them

Put them in your repo. Not Confluence. Not Notion. Not a wiki.

docs/
  adr/
    0001-use-next-js-for-storefront.md
    0002-postgres-over-dynamodb-for-order-data.md
    0003-feature-flags-with-launchdarkly.md
    ...
    template.md

If someone is in the repo, they're one directory away from understanding why the code is the way it is. If the ADRs live in a wiki, they need to know the wiki exists, have access to it, and remember to check it. That's three friction points too many.

When someone proposes a new ADR, it goes through a pull request. The team reviews it. People push back on the options considered. Someone catches a constraint that was missed. The ADR gets better before it's merged. You can't do that with a Confluence page.

When a decision gets superseded, the git history shows exactly what changed and when. Try getting that from a wiki edit history.

The CLI Scaffolding Script

The full repo is at github.com/jonoherrington/new-adr ... the Bash script, the PowerShell version, and the template. Clone it or copy what you need.

If you want to see how it's put together, here's the Bash script. Run instructions for Windows are in the same section, right after it.

#!/bin/bash
# save as: scripts/new-adr.sh
# usage: ./scripts/new-adr.sh "Use Redis for session management"
 
set -e
 
ADR_DIR="docs/adr"
TEMPLATE="$ADR_DIR/template.md"
 
TITLE="$1"
if [ -z "$TITLE" ]; then
  echo "Usage: $0 \"Title of the decision\""
  exit 1
fi
 
mkdir -p "$ADR_DIR"
 
LOCKDIR="$ADR_DIR/.new-adr.lock"
if ! mkdir "$LOCKDIR" 2>/dev/null; then
  echo "Another new-adr is running, or a stale lock exists at $LOCKDIR" >&2
  echo "Wait for the other run to finish, or remove that directory if a run crashed." >&2
  exit 1
fi
trap 'rmdir "$LOCKDIR" 2>/dev/null || true' EXIT INT TERM HUP
 
# Max numeric prefix among files like 0007-foo.md or 10000-bar.md (ignores template.md, etc.)
LAST=$(ls -1 "$ADR_DIR" 2>/dev/null | grep -E '^[0-9]+-' | sed 's/-.*//' | sort -n | tail -1)
if [ -z "$LAST" ]; then
  NEXT_NUM=1
else
  NEXT_NUM=$((10#$LAST + 1))
fi
 
# ASCII-only slug (matches PowerShell); non-ASCII letters become separators
SLUG=$(export LC_ALL=C; printf '%s' "$TITLE" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/-$//')
if [ -z "$SLUG" ]; then
  SLUG="untitled"
fi
 
NEXT=$(printf '%04d' "$NEXT_NUM")
FILENAME="$ADR_DIR/$NEXT-$SLUG.md"
while [ -e "$FILENAME" ]; do
  NEXT_NUM=$((NEXT_NUM + 1))
  if [ "$NEXT_NUM" -gt 999999 ]; then
    echo "Could not find a free ADR number below 1000000." >&2
    exit 1
  fi
  NEXT=$(printf '%04d' "$NEXT_NUM")
  FILENAME="$ADR_DIR/$NEXT-$SLUG.md"
done
 
DATE=$(date +%Y-%m-%d)
 
cat > "$FILENAME" << EOF
# ADR-$NEXT: $TITLE
 
## Status
 
{Proposed | Accepted | Deprecated | Superseded by ADR-XXX}
 
## Date
 
$DATE
 
## Context
 
What is the situation? What forces are at play? What constraints exist?
Write this for someone who has zero context on the problem. Because in
18 months, that someone is you.
 
## Decision
 
What did we decide? Be specific. Name the technology, the pattern, the
approach. Don't hedge.
 
## Consequences
 
What becomes easier? What becomes harder? What are we explicitly accepting
as tradeoffs? What doors does this close?
 
## Options Considered
 
### Option A: {Name}
- How it works
- Pros
- Cons
- Why we didn't choose it
 
### Option B: {Name}
- How it works
- Pros
- Cons
- Why we didn't choose it
 
### Option C: {Chosen} ✓
- How it works
- Pros
- Cons
- Why we chose it
EOF
 
echo "Created: $FILENAME"

macOS and Linux: mark the script executable, then run it:

chmod +x scripts/new-adr.sh
./scripts/new-adr.sh "Use Redis for session management"
# Created: docs/adr/0007-use-redis-for-session-management.md

Windows: chmod is a Unix command; it is not available in PowerShell, and Windows does not use the same executable-bit idea for scripts. You still have two straightforward options:

  • Git Bash (installed with Git for Windows): run the same script with Bash — no chmod needed:
bash scripts/new-adr.sh "Use Redis for session management"
  • PowerShell: use a PowerShell version of the same logic (this repo includes new-adr.ps1 at the project root):
.\new-adr.ps1 "Use Redis for session management"

If you use Git and want the executable bit stored for teammates on macOS or Linux, run this once from any shell that has Git: git update-index --chmod=+x scripts/new-adr.sh.

A few sharp edges worth handling in automation: concurrency (two people run the script at once), collisions (the next number is free but that exact filename already exists because someone hand-edited numbering), very large sequences (teams that blow past ADR-9999), and Unicode titles (slug should be predictable in code review). The script above uses a lock directory under docs/adr/.new-adr.lock so two terminals do not pick the same sequence number; if a run crashes, delete that empty folder. If the target path already exists, it bumps the number until it finds a free filename. Numeric prefixes are parsed as integers, so ADR-10000 works the same as ADR-0007. Slugs keep ASCII letters and digits only so Bash and PowerShell stay aligned.

Now you have a pre-numbered, pre-dated, template-ready file. Open it, fill in the sections, submit a PR.

The Mistakes I Made

Mistake 1: Writing ADRs after the decision was implemented.

For the first few months, ADRs were retrospective. Someone would finish a project and then write the ADR as documentation. The problem is obvious in hindsight. By the time you're writing it after the fact, you've forgotten half the options you considered and most of the context that drove the decision. The ADR becomes a justification for what you already built instead of a record of how you thought through the problem.

Write the ADR before or during the decision. Submit it as a PR. Let the team review it. The review process often surfaces options and constraints nobody had considered. That's the whole point.

Mistake 2: Making them too long.

My early ADRs were 3,000+ word documents. Nobody read them. An ADR should take 10 to 30 minutes to write and 5 minutes to read. If you're writing more than a page, you're either documenting multiple decisions (split them up) or including implementation details that belong in the code.

Mistake 3: Not updating the status.

We had ADRs marked "Accepted" that described systems we'd completely replaced. A new engineer followed one of these outdated ADRs and spent three days building on assumptions that no longer held. Now we have a rule … when you supersede a decision, you update the old ADR's status in the same PR. One line change. Accepted becomes Superseded by ADR-0023. That one line saves days.

Mistake 4: Only architects writing them.

ADRs aren't an architect artifact. Any engineer making a decision that affects the team should write one. "Should we use library X or library Y for date handling?" is a valid ADR. "Should we add an index to this table?" is a valid ADR. The threshold is simple … if someone will wonder "why did we do it this way?" in six months, write an ADR.

When to Write One (and When Not To)

The threshold is simple. If you're choosing between multiple viable approaches, if the decision will outlast the sprint, or if you'll find yourself explaining it in Slack six months from now ... write an ADR. If there's only one reasonable option, or the decision is easily reversible and scoped to a single function ... skip it.

The test I use: would a new engineer joining the team need to understand why we did it this way? If yes, write the ADR.

Making It Stick

We got the template right fairly quickly. The harder problem was getting the team to actually use it. Here's what worked.

Add it to the PR checklist

If a PR introduces a new pattern, a new dependency, or a new architectural boundary … the checklist asks "Does this need an ADR?" Most of the time the answer is no. But the question being there normalizes the practice.

Reference ADRs in code reviews

When someone asks "why did we do it this way?" in a PR comment, the answer should be a link to the ADR. Once people start getting their questions answered by ADRs instead of by Slack threads, they start seeing the value.

Celebrate the saves

When an ADR prevents wasted work … like the session management example I opened with … call it out. In the retro, in the team channel, wherever. People do what gets recognized.

Keep the template visible

The template.md file in the docs/adr/ directory means nobody has to remember the format. They copy the template, fill it in, submit. Zero friction.

The Compound Effect

I've been running ADRs across my teams for a few years now. The effect compounds in ways I didn't expect.

Onboarding accelerated. New engineers read the ADR directory in their first week and understand not just what the system does, but why it's built the way it is. The context that used to live in one person's head now lives in the repo.

Decision quality improved. The act of writing down your options and tradeoffs forces clearer thinking. I've watched engineers change their own minds while writing an ADR because the process of articulating the tradeoffs made the right choice obvious.

Fewer repeated debates. The same architectural argument doesn't surface every six months because the last time it was settled, the reasoning was captured. If the constraints changed, great … update the ADR. If they didn't, the conversation is a 5-minute read instead of a 2-hour meeting.

The knowledge stopped walking out the door. That's the real win. When someone leaves, their decisions stay.

This post is a tactical companion to Your Best Engineer Is Your Biggest Risk, which covers the leadership side of the bus factor problem. If you want the "why," start there. If you want the "how," you're already here.