Every codebase contains decisions that are obvious to the people who made them and mysterious to everyone else six months later.
Why does the billing module publish events instead of calling the CRM directly? Why is the API gateway allowed to import from shared contracts but not from feature packages? Why did the team keep the monolith instead of extracting a service? The answers probably existed once, in a Slack thread, a meeting, a pull request comment, or someone's memory.
Then the team changed. The thread disappeared. The PR got buried. The person who knew the context moved to another project.
That is how technical context gets lost. And when context disappears, teams repeat old debates, undo good decisions, and introduce architecture drift without realizing they are violating an intentional design.
Architecture Decision Records, usually called ADRs, are a lightweight way to prevent that.
What Is an Architecture Decision Record?
An Architecture Decision Record is a short document that captures one significant technical decision, the context behind it, the options considered, and the consequences the team accepts.
An ADR is not a long architecture document. It is not a polished strategy deck. It is a record of a real decision, written close to the moment the decision is made.
A useful ADR answers five questions:
- What decision did we make?
- Why did we need to make it?
- Which options did we consider?
- Why did we choose this option?
- What trade-offs are we accepting?
The goal is not to prove the team made the perfect decision. The goal is to make the decision legible to future maintainers.
Why ADRs Matter
Most architecture problems are not caused by a lack of intelligence. They are caused by missing context.
A new developer sees a module boundary and does not know why it exists. A product deadline appears, and someone bypasses a service because the direct import is faster. A reviewer approves the change because the diff looks reasonable. Three months later, the codebase has a dependency path nobody intended.
This is exactly how code reviews let architecture debt slip through. Reviewers inspect the local change, but they do not always know the historical reason behind a boundary.
ADRs give reviewers and maintainers a source of truth. When a PR crosses a boundary, the reviewer can point to the decision record instead of relying on memory or personal preference.
What Should Become an ADR?
Not every choice deserves an ADR. You do not need a decision record for naming a helper function or choosing between two equivalent utility libraries.
Write an ADR when the decision changes how the system is shaped.
Good ADR topics include:
- Choosing a monolith, modular monolith, or microservices approach
- Defining module boundaries inside a monorepo
- Selecting a database, queue, framework, or authentication provider
- Deciding how services communicate
- Introducing a shared package or public internal API
- Changing the deployment model
- Accepting a temporary architectural compromise
- Defining which dependencies are forbidden
The simplest rule: if a future developer might ask "why is it like this?", write an ADR.
A Practical ADR Template
A useful ADR should be short enough that people actually write it. Start with this structure:
# ADR 004: Keep billing inside the modular monolith
## Status
Accepted
## Context
The billing domain touches subscriptions, invoices, usage limits, and customer lifecycle events. The team considered extracting it into a separate service because billing changes are sensitive and operationally important.
## Decision
We will keep billing inside the modular monolith for now, behind a clear module boundary and an internal event interface.
## Options Considered
- Extract billing into a separate service
- Keep billing as a normal application module
- Keep billing inside the monolith with explicit boundaries
## Consequences
This keeps local development and deployments simpler. It also means billing code must not import directly from unrelated feature modules. If billing coupling grows, we will revisit extraction.
This is enough. The value comes from consistency, not length.
Where ADRs Should Live
ADRs should live in the repository, close to the code they explain.
A common structure is:
docs/
adr/
0001-use-postgres.md
0002-modular-monolith-boundaries.md
0003-github-pr-quality-gates.md
Putting ADRs in the repository has three advantages.
First, ADRs are versioned with the code. If the architecture changes, the decision history changes in the same place.
Second, ADRs can be reviewed in pull requests. A meaningful architecture change should include both code and the decision record that explains it.
Third, ADRs become part of onboarding. New developers can read the decision trail instead of piecing together history from old messages.
How ADRs Reduce Architecture Drift
Architecture drift happens when the actual code gradually diverges from the intended architecture. ADRs do not prevent drift by themselves, but they make the intended architecture explicit.
For example, suppose an ADR says:
Feature packages may depend on shared contracts, but shared contracts must not depend on feature packages.
That sentence gives you something concrete to enforce. You can translate it into dependency rules, review checklists, or automated PR checks.
Without the ADR, the rule is just tribal knowledge. With the ADR, the rule becomes part of the codebase's operating model.
This is where ADRs pair well with automated checks. The ADR explains the "why"; tools enforce the "what". A quality gate on GitHub can block forbidden imports, while the ADR explains why that import is forbidden in the first place.
Common ADR Mistakes
The most common mistake is writing ADRs like legal documents. If an ADR takes two hours to produce, nobody will keep doing it. Keep them plain, practical, and close to the decision.
Another mistake is rewriting history. ADRs should capture trade-offs honestly. If the team chose the simpler option because of delivery pressure, say that. Future maintainers need reality, not a polished version of reality.
A third mistake is treating ADRs as permanent truth. Architecture evolves. When a decision changes, do not edit the old ADR until the history disappears. Mark it as superseded and write a new one.
## Status
Superseded by ADR 009: Extract billing into a separate service
The old decision still matters because it explains why the system looked the way it did at that time.
Connecting ADRs to Code Review
ADRs become most useful when they show up in everyday workflow.
Add a small section to your PR template:
## Architecture impact
- Does this PR change module boundaries?
- Does it introduce a new dependency direction?
- Does it require a new or updated ADR?
This does not need to be bureaucratic. Most PRs will answer "no". But when the answer is "yes", the team catches architecture changes before they quietly become permanent.
For larger changes, link the ADR directly in the PR description. Reviewers can evaluate the implementation and the reasoning together.
How ReposLens Helps
ReposLens cannot write your ADRs for you, but it can make the decisions easier to validate.
When you define boundaries in an ADR, ReposLens helps you see whether the code still respects them. If a shared package starts depending on a feature package, the dependency graph makes the violation visible. If a PR introduces a new circular dependency, the architecture check can catch it before merge.
This closes the loop:
- ADRs document the intended architecture
- Dependency visualization reveals the actual architecture
- PR checks prevent accidental divergence
That combination is much stronger than documentation or tooling alone.
Start Small
You do not need to document every decision your team has ever made. Start with the next significant architecture decision.
Create docs/adr/0001-your-decision.md. Keep it under one page. Link it from the PR. Revisit it when the architecture changes.
The payoff arrives later, when someone asks why the system is shaped the way it is and the answer is not hidden in memory. It is right there, in the repository, next to the code it protects.
Related Articles
How to Detect Dead Code in a Large Codebase Before It Slows You Down
A practical guide to finding dead code in large applications using usage signals, dependency graphs, static analysis, and safe removal workflows.
Continue readingMicroservices vs Monolith: How to Actually See Your Architecture
Monolith or microservices? The real question is: can you see what you actually have? Learn how to visualize, compare, and decide with confidence.
Continue readingHow to Detect Circular Dependencies in Your TypeScript Project (and Fix Them)
Learn 3 practical methods to detect circular dependencies in TypeScript: madge CLI, ESLint import/no-cycle, and automated PR checks with ReposLens. Includes fix patterns.
Continue reading