The decision that keeps getting re-litigated
Every frontend team has one: the architecture choice that gets debated every quarter. "Why didn't we use WebSockets?" "Should we switch to micro-frontends?" "Why is the state management library X instead of Y?"
The debate restarts because nobody wrote down why the decision was made, what was considered, and what trade-offs were accepted. Six months later, the context is gone, the original engineers may have moved on, and the team re-argues from scratch.
Architecture Decision Records prevent this. Not as process theater, but as a tool that gives your future self (and your team) the reasoning behind today's choices.
The template that actually gets used
Most ADR templates are too long. Engineers skip them because they feel like paperwork. Here is a template short enough that people will actually write it:
# ADR-{number}: {Short descriptive title}
**Status:** Proposed | Accepted | Deprecated | Superseded by ADR-{n}
**Date:** YYYY-MM-DD
**Authors:** {who made the call}
## Context
What is the situation? What forces are at play? What constraints exist?
Write this as if explaining to a new teammate who joins in 6 months.
## Options Considered
### Option A: {name}
- Pros: ...
- Cons: ...
- Estimated effort: ...
### Option B: {name}
- Pros: ...
- Cons: ...
- Estimated effort: ...
## Decision
What did we choose and why? Be specific about which factors tipped the scale.
## Consequences
What becomes easier? What becomes harder? What pain are we consciously
accepting? What will we need to revisit and when?
The "Options Considered" section is what separates a useful ADR from a post-hoc justification. If you only document the winner, future readers cannot tell whether alternatives were considered — they will assume you did not think it through.
A bad ADR vs. a good ADR
Bad ADR: vague, no alternatives, no consequences
# ADR-12: Use Redux for state management
## Context
We need state management.
## Decision
We will use Redux.
## Consequences
We will have centralized state.
This tells you nothing. Why Redux? What was the alternative? What pain does this create? A new engineer reading this is no better off than if it did not exist.
Good ADR: specific, shows reasoning, honest about trade-offs
# ADR-12: Use Zustand for client state, React Query for server state
**Status:** Accepted
**Date:** 2026-02-15
**Authors:** Sarah Chen, Marcus Johnson
## Context
Our app has grown from 3 to 12 pages. We currently use React context
for everything — auth state, UI preferences, server cache, form state.
This causes:
- Unnecessary re-renders: changing the theme re-renders components
that only read auth state (measured: 340ms layout shift on settings
toggle)
- No cache invalidation: server data is stale until manual refresh
- Testing friction: every test needs a full provider tree
The team has 4 frontend engineers. We need something with a low
learning curve that can be adopted incrementally.
## Options Considered
### Option A: Redux Toolkit + RTK Query
- Pros: Battle-tested, excellent DevTools, RTK Query handles
caching/invalidation
- Cons: Boilerplate for actions/reducers, steeper learning curve for
the 2 engineers who haven't used Redux, requires migrating all
state at once (context + Redux interop is messy)
- Estimated effort: 3-4 weeks full migration
### Option B: Zustand (client) + React Query (server)
- Pros: Minimal API surface (single create() call for stores), React
Query handles caching/invalidation/background refresh, both can be
adopted one page at a time alongside existing context
- Cons: Two libraries instead of one, fewer community examples for
the combined pattern, Zustand DevTools are less mature
- Estimated effort: 1-2 weeks for core pages, remainder migrated
incrementally
### Option C: Jotai (atomic state)
- Pros: Very fine-grained reactivity, tiny bundle
- Cons: Atomic model is a paradigm shift for the team, server state
still needs a separate solution, fewer established patterns for
larger apps
- Estimated effort: 3 weeks + team ramp-up
## Decision
Option B: Zustand for client state, React Query for server state.
The deciding factors:
1. Incremental adoption — we cannot afford a big-bang migration with
Q2 feature commitments
2. Server state separation — React Query's stale-while-revalidate
and background refetch solve our #1 user-facing issue (stale data)
3. Team familiarity — 3 of 4 engineers have used React Query before
## Consequences
### What gets easier
- Server data freshness (React Query handles this automatically)
- Re-render performance (Zustand selectors prevent unnecessary
re-renders)
- Testing (stores can be tested without React rendering)
### What gets harder
- Two libraries to maintain and keep updated
- New engineers need to learn the Zustand + React Query mental model
- Some patterns (optimistic updates spanning client + server state)
require coordinating both libraries
### What we accept
- We will live with two state libraries rather than one unified
solution. If this becomes painful, we will revisit in Q4.
- We will NOT migrate all existing context immediately. Pages will
be migrated as they are modified for other reasons.
### Review trigger
- Revisit if team grows beyond 8 frontend engineers (coordination
cost may favor a more opinionated framework)
- Revisit if Zustand DevTools do not improve by Q4
Notice what makes this useful:
- A new engineer can read it in 5 minutes and understand the full context
- The numbered deciding factors make the logic auditable
- The "review trigger" section prevents the decision from becoming sacred — it tells you when to reconsider
- The effort estimates show that practical constraints (Q2 commitments) shaped the choice, not just technical preference
When to write an ADR
Not every decision needs one. Write an ADR when:
┌─────────────────────────────────────────────────────────┐
│ Write an ADR when: │
│ │
│ ✓ The decision is hard to reverse (framework, DB, │
│ protocol, deployment model) │
│ │
│ ✓ The decision will be questioned later (library │
│ choices, architecture patterns, build tool changes) │
│ │
│ ✓ Multiple viable options exist and reasonable │
│ engineers could disagree │
│ │
│ ✓ The decision affects other teams or shared systems │
│ │
│ Skip an ADR when: │
│ │
│ ✗ The decision is easily reversible (CSS approach, │
│ variable naming, file organization within a module) │
│ │
│ ✗ There is an obvious industry standard with no │
│ meaningful alternatives │
│ │
│ ✗ You are the only person who will ever need to know │
└─────────────────────────────────────────────────────────┘
Real ADR examples for common frontend decisions
ADR: Notification delivery transport
# ADR-07: SSE for notification delivery, HTTP for read-state writes
**Status:** Accepted
**Date:** 2026-01-20
## Context
Users need real-time notifications across multiple tabs.
Current polling approach causes 2-second delays and generates
~4,000 requests/minute across the user base.
Multi-tab synchronization is required — marking a notification
as read in one tab should reflect in all tabs.
Infrastructure budget: we can add one new service, not a full
WebSocket cluster.
## Options Considered
### WebSockets
- Pros: Bidirectional, well-understood
- Cons: Requires sticky sessions or a pub/sub layer, connection
management is complex with multiple tabs, our load balancer
needs reconfiguration
- Effort: 2-3 weeks + infra changes
### Server-Sent Events (SSE)
- Pros: Works over HTTP/2 (shared connection), auto-reconnect
built in, unidirectional (simpler server), works through our
existing load balancer
- Cons: No binary data, no upstream channel (but we don't need one
for notifications), IE11 not supported (acceptable)
- Effort: 1 week
### Polling with aggressive caching
- Pros: Simplest implementation
- Cons: Minimum 1-second delay, still generates high request volume,
does not solve multi-tab sync
- Effort: 2 days, but doesn't solve the core problems
## Decision
SSE for delivery. Read-state mutations stay as regular HTTP POST.
Multi-tab sync via BroadcastChannel API — when one tab marks a
notification as read, it broadcasts to other tabs so they update
without a server round-trip.
## Consequences
- Simpler server: one-way stream, no connection state management
- Multi-tab works through browser APIs, not server coordination
- We accept: no bidirectional channel. If we later need presence
indicators or typing status, we'll need WebSockets then.
- Review trigger: if product adds collaborative features (shared
cursors, live editing), revisit transport choice.
ADR: Modular monolith vs. micro-frontends
# ADR-15: Modular monolith with lazy-loaded feature modules
**Status:** Accepted
**Date:** 2026-03-01
## Context
Engineering has grown from 2 to 5 frontend teams. The main app is
a single Vite build with ~180 components. Pain points:
- Build times: 4 minutes (was 45 seconds 8 months ago)
- Merge conflicts: 3-5 per week in shared files
- Deployment coupling: Team A's bug blocks Team B's release
The VP of Engineering asked if we should adopt micro-frontends.
## Options Considered
### Micro-frontends (Module Federation)
- Pros: Independent deploys, team autonomy, technology freedom
- Cons: Shared state becomes a coordination problem, design system
consistency requires enforcement, runtime loading adds latency,
we need a shell app team (we don't have one), debugging across
boundaries is harder
- Effort: 8-12 weeks for shell + first 2 modules, ongoing
platform tax estimated at 0.5 engineer
### Modular monolith with feature boundaries
- Pros: Single build (Vite handles code splitting), shared types,
enforced boundaries via linting rules, no runtime federation
overhead, no shell team needed
- Cons: Still one deploy (though feature flags mitigate this),
teams share a build pipeline, requires discipline to maintain
module boundaries
- Effort: 3-4 weeks to establish boundaries and lint rules
### Monorepo with separate apps
- Pros: Independent builds and deploys, shared packages
- Cons: Shared component updates require coordinated releases,
duplication risk, need monorepo tooling (Nx/Turborepo)
- Effort: 6-8 weeks
## Decision
Modular monolith with strict boundaries.
Rationale: Our pain is about code organization and merge conflicts,
not about deploy independence. Feature flags already let teams ship
independently. The micro-frontend tax (shell team, runtime
coordination, debugging complexity) is not justified by our current
scale.
Boundaries enforced by:
- ESLint boundaries plugin (modules cannot import from each other
except through public APIs)
- CODEOWNERS file (each module directory owned by one team)
- Route-based code splitting (each module is a lazy route)
## Consequences
### What gets easier
- Merge conflicts (each team works in their own module directory)
- Build times (Vite only rebuilds changed modules)
- Onboarding (clear module boundaries, one build tool)
### What we accept
- Teams share a deploy. Feature flags handle the independence need.
- If we grow to 10+ teams, module boundaries may not be enough.
- One team's bad dependency can still affect build times.
### Review trigger
- Revisit if team count exceeds 8 or if feature flag complexity
becomes a burden
- Revisit if any team needs a fundamentally different tech stack
How ADRs change your career trajectory
Writing ADRs is one of the clearest signals of senior engineering behavior:
Naming constraints — "We chose X because of constraints A, B, C" shows you understand the problem space, not just the solution space.
Making trade-offs explicit — Junior engineers look for the "right" answer. Senior engineers document which trade-offs they accepted and why.
Creating institutional memory — When you leave a team, your ADRs remain. New engineers can understand years of decisions in an afternoon.
Building influence — In cross-functional discussions, "here is our ADR for this" immediately elevates the conversation. It shows rigor.
Interview leverage — When asked "tell me about an architecture decision," having a written ADR means you can describe the context, options, decision, and consequences with precision. That is exactly what staff-level interviews look for.
Where to store them
Keep ADRs in the repo, not in Confluence or Notion. They should live next to the code they describe:
docs/
adr/
0001-zustand-react-query-state.md
0002-sse-notification-delivery.md
0003-modular-monolith.md
0004-vitest-over-jest.md
template.md
Numbered, in the repo, versioned with git. When a decision is superseded, update the status to "Superseded by ADR-{n}" rather than deleting it — the historical reasoning is still valuable.
Practice making these decisions
The best way to get better at ADRs is to practice the decisions they capture:
- Micro-Frontend Architecture — the modular monolith vs. micro-frontend decision
- Design a Reusable Component Library — API design, versioning, and adoption trade-offs
- The Monolith Tipping Point — a War Room mission about this exact decision under pressure
- The Architecture Bomb — reviewing someone else's architecture decision with rigor and empathy
Related reading:
- 5 AI Patterns Every Frontend Engineer Will Build in 2026 — architecture decisions in AI feature design
- Building a Streaming AI Chat UI — transport and state management decisions
LLM-friendly summary
A practical ADR guide for frontend engineers with a lightweight template and examples involving notifications, client-side inference, and micro-frontends.