Skip to content

Architecture Decision Records

expert14 min read

The Question Nobody Can Answer: "Why Did We Build It This Way?"

Six months from now, a new engineer joins your team. They look at your state management setup -- a mix of React Context, URL state, and server state -- and ask: "Why not Zustand? Why not Redux? This seems complicated."

Nobody remembers. The person who made the decision left. The Slack thread is buried. The PR description says "refactor state management" with no explanation of the alternatives considered.

This is the problem Architecture Decision Records (ADRs) solve. They capture why a decision was made, not just what was decided.

Mental Model

Think of ADRs like a lab notebook in science. Scientists do not just record results -- they record hypotheses, methodology, failed approaches, and reasoning. If an experiment needs to be repeated or questioned years later, the notebook explains everything. An ADR is a lab notebook for your architecture. It explains the hypothesis (context), the experiment (decision), and the expected outcomes (consequences).

What Goes in an ADR

An ADR captures a single architectural decision with four essential parts:

1. Context

What situation are you facing? What constraints exist? What triggered the need for a decision?

2. Decision

What did you decide? Be specific. Not "use a state management library" but "use React Context for auth state, URL searchParams for filter state, and Server Components for server state, with no additional state library."

3. Consequences

What are the trade-offs? Both positive and negative. What becomes easier? What becomes harder?

4. Status

Is this decision active, deprecated, or superseded by a later ADR?

ADR Template

# ADR-001: State Management Strategy

**Status:** Accepted
**Date:** 2024-03-15
**Deciders:** Sarah (tech lead), Marcus (senior FE), Priya (staff eng)

## Context

Our application has grown to 40+ components with state needs spanning
auth, theme, course progress, quiz state, and filter state. Currently,
state is managed ad-hoc with useState, prop drilling, and one global
Zustand store that has become a dumping ground for unrelated state.

The team is spending increasing time debugging state-related issues
and arguing about where new state should live.

## Decision

We will use a layered state strategy with no additional state library:

- **Server state**: React Server Components fetch data on the server.
  No client-side data fetching library needed for initial loads.
- **URL state**: Filter, sort, pagination, and search parameters stored
  in URL searchParams via useSearchParams. Shareable, bookmarkable.
- **Auth state**: React Context provider at the root layout.
  Read-only for most components.
- **Local UI state**: useState/useReducer for component-specific state
  (modals, form inputs, accordion open/close).
- **Remove Zustand**: Migrate the existing Zustand store to the above
  categories over the next 2 sprints.

## Alternatives Considered

### Zustand (keep and organize)
- Pro: Already in the codebase, team knows it
- Con: Encourages client-side state for data that should be server state.
  Store has become a dumping ground. No natural categorization.

### Redux Toolkit
- Pro: Strong patterns (slices, RTK Query), large ecosystem
- Con: 40KB+ added to bundle for patterns we do not need. Server
  Components make RTK Query redundant for our use case.

### Jotai
- Pro: Atomic, composable, tiny bundle
- Con: Another library to learn when our actual problem is
  architectural (wrong state in wrong place), not tooling.

## Consequences

### Positive
- Zero additional client JS for state management
- Server Components handle the majority of data fetching
- URL state makes pages bookmarkable and shareable
- Clear rules for where new state goes (decision tree in docs)

### Negative
- Team needs to learn the Server Component data fetching pattern
- Some complex client interactions (quiz timer, drag-and-drop)
  may need local state management that is more verbose without Zustand
- Migration from Zustand will take 2 sprints of incremental work
Quiz
What is the most important section of an ADR for future readers?

When to Write an ADR

Not every decision needs an ADR. Use them for decisions that are:

  • Hard to reverse: choosing a framework, database, deployment strategy
  • Frequently questioned: "why do we do it this way?" being asked repeatedly
  • Cross-team impact: decisions that affect multiple teams or packages
  • Trade-off heavy: decisions where the alternatives were genuinely competitive
DecisionNeeds ADR?Why
Choose Next.js over RemixYesFramework choice affects everything and is very hard to reverse
Use React Context over Zustand for authYesTrade-off heavy, frequently questioned, impacts architecture
Use Tailwind CSS for stylingYesPervasive decision, hard to reverse, alternative approaches exist
Name a component CourseCard vs CourseListItemNoEasy to rename, low impact, no trade-offs
Use date-fns over dayjs for formattingMaybeLow impact but if the team debated it, write it down to avoid re-debating
Choose between REST and GraphQLYesMajor architectural decision with significant consequences
The re-debate threshold

If you have debated a decision more than once, it needs an ADR. The ADR ends the re-litigation by documenting the reasoning. Future debates start from where the last one ended, not from scratch.

Living Documentation

ADRs are not write-and-forget. They evolve with the project.

Superseding an ADR

When a decision changes, do not edit the original ADR. Create a new one that supersedes it:

# ADR-007: Migrate from REST to tRPC for Internal APIs

**Status:** Accepted (supersedes ADR-003)
**Date:** 2024-09-01

## Context

ADR-003 chose REST with OpenAPI codegen for our API layer. Since then:
- We migrated to a full TypeScript backend (was Python)
- We adopted a monorepo structure
- OpenAPI codegen adds a 30-second build step that frustrates developers

With both frontend and backend in TypeScript in a monorepo, tRPC
eliminates the codegen step entirely while providing the same type safety.

## Decision

Migrate internal API calls from REST + OpenAPI codegen to tRPC.
Keep REST for:
- Webhook endpoints consumed by third-party services
- Public API (if we build one)

The original ADR-003 is updated to show Status: Superseded by ADR-007. The history is preserved -- you can trace the evolution of the decision.

ADR Index

Keep an index file that lists all ADRs with their status:

# Architecture Decision Records

| ID | Title | Status | Date |
|----|-------|--------|------|
| 001 | State Management Strategy | Accepted | 2024-03-15 |
| 002 | Monorepo with Turborepo | Accepted | 2024-03-20 |
| 003 | REST with OpenAPI Codegen | Superseded by 007 | 2024-04-01 |
| 004 | Tailwind CSS 4 for Styling | Accepted | 2024-04-10 |
| 005 | Feature-Based Project Structure | Accepted | 2024-05-01 |
| 006 | Micro-frontend Evaluation | Rejected | 2024-06-15 |
| 007 | Migrate to tRPC for Internal APIs | Accepted | 2024-09-01 |

"Rejected" ADRs are valuable too. ADR-006 documents why the team evaluated micro-frontends and chose not to adopt them. That prevents the next VP from proposing the same thing without new information.

Quiz
An ADR from 6 months ago chose Zustand for state management. The team now wants to switch to React Context. What is the correct process?

ADR Tooling

adr-tools (CLI)

A simple shell script that manages ADRs as numbered Markdown files:

adr new "Use React Server Components for data fetching"
# Creates docs/adr/0008-use-react-server-components-for-data-fetching.md

adr list
# Lists all ADRs with status

adr link 8 "Supersedes" 3
# Links ADR 8 to ADR 3

log4brains

A more visual tool that generates a static site from your ADRs:

npx log4brains init
npx log4brains adr new
npx log4brains preview

It creates a searchable, browsable website of all your ADRs with a timeline view, status filtering, and Markdown rendering.

Just Markdown Files

Honestly? For most teams, a docs/adr/ folder with numbered Markdown files is enough. The tooling is nice but not essential. What matters is the habit of writing them, not the tool.

docs/
  adr/
    000-template.md
    001-state-management-strategy.md
    002-monorepo-with-turborepo.md
    003-rest-with-openapi-codegen.md
    ...
    index.md

Decision Fatigue and Defaults

Every decision costs cognitive energy. ADRs help by establishing defaults -- pre-made decisions that apply unless there is a strong reason to deviate.

# ADR-000: Project Defaults

These are the default choices for this project. Deviating requires
a new ADR with justification.

- **Styling**: Tailwind CSS 4 with CSS variables
- **State**: Server Components for data, URL for filters, Context for auth
- **Testing**: Vitest + RTL for unit, Playwright for E2E
- **API**: tRPC for internal, REST for external
- **Components**: Server Components by default, 'use client' only when needed
- **Formatting**: Prettier with project config, no overrides
- **Package manager**: pnpm strict mode

When a new engineer asks "which testing library should I use?", the answer is in ADR-000. No discussion needed. They can deviate, but they need to write an ADR explaining why.

ADRs in Open Source

Many successful open source projects use ADRs publicly. The React team documents major decisions (Server Components, React Compiler). Next.js has RFCs that function like ADRs. The Rust language has a formal RFC process that is essentially ADRs with community review.

For your team, consider making ADRs part of the PR process for architectural changes. Before the code is written, the ADR is reviewed. This catches "we should have done it differently" feedback before the code exists, not after.

Common Trap

Do not write ADRs for decisions that are easily reversible and low-impact. If renaming a utility function requires an ADR, the process is too heavy and the team will stop writing them. Reserve ADRs for decisions that are expensive to change or that affect multiple people. The goal is decision quality, not bureaucratic completeness.

Key Rules
  1. 1Write ADRs for decisions that are hard to reverse, frequently questioned, or cross-team
  2. 2Always document alternatives considered with specific reasons for rejection
  3. 3Never edit old ADRs -- supersede them with new ones to preserve decision history
  4. 4Establish project defaults in ADR-000 so most decisions are pre-made
  5. 5Keep ADRs in the repo (not Confluence, not Notion) so they are versioned with the code
What developers doWhat they should do
Writing ADRs after the code is shipped, as retroactive documentation
ADRs written after the fact miss rejected alternatives and the reasoning behind the decision. Writing them before implementation forces explicit thinking about trade-offs and alternatives.
Writing ADRs before implementation, as part of the design review process
Editing old ADRs to reflect current decisions
Editing destroys the decision history. Future engineers cannot understand why the decision evolved. Superseding preserves the trail of reasoning.
Creating new ADRs that supersede old ones
Writing ADRs for every minor technical choice
Too many ADRs create noise and decision fatigue. Nobody reads 200 ADRs. Keep the count low (20-50 for a mature project) and each one high-signal.
Writing ADRs only for high-impact, hard-to-reverse decisions