Architecture Decision Records
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.
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
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
| Decision | Needs ADR? | Why |
|---|---|---|
| Choose Next.js over Remix | Yes | Framework choice affects everything and is very hard to reverse |
| Use React Context over Zustand for auth | Yes | Trade-off heavy, frequently questioned, impacts architecture |
| Use Tailwind CSS for styling | Yes | Pervasive decision, hard to reverse, alternative approaches exist |
| Name a component CourseCard vs CourseListItem | No | Easy to rename, low impact, no trade-offs |
| Use date-fns over dayjs for formatting | Maybe | Low impact but if the team debated it, write it down to avoid re-debating |
| Choose between REST and GraphQL | Yes | Major architectural decision with significant consequences |
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.
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.
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.
- 1Write ADRs for decisions that are hard to reverse, frequently questioned, or cross-team
- 2Always document alternatives considered with specific reasons for rejection
- 3Never edit old ADRs -- supersede them with new ones to preserve decision history
- 4Establish project defaults in ADR-000 so most decisions are pre-made
- 5Keep ADRs in the repo (not Confluence, not Notion) so they are versioned with the code
| What developers do | What 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 |