Why ADRs Are the Most Important File Nobody Writes

By Charlin Joe · 25 July 20260 views
Why ADRs Are the Most Important File Nobody Writes

Why ADRs Are the Most Important File Nobody Writes

A new developer joins the project. It's their second week. They're looking at the order service and they notice something that feels off.

getOrdersByUser() filters by where('userId', isEqualTo: uid). That's it. No null check. No explicit exclusion of guest orders. To someone reading the code fresh, it looks like a potential bug — shouldn't you explicitly filter out orders where userId is null?

So they add the null check. It seems like the safe, defensive thing to do. They submit a PR. The PR is approved by a reviewer who doesn't know the guest checkout history. The change merges.

Six months later, the analytics team notices that their guest-versus-registered breakdown query is returning wrong numbers. The segment filter uses userId != null. The orders that should be in the guest segment are now also appearing in the registered segment under some conditions. There's a threading issue nobody fully understands. The root cause, after three days of investigation, is the null check that was added in week two — a null check that was intentionally absent because the isEqualTo: uid filter already excluded guest orders, and adding an explicit null check changed the query semantics in a subtle way that nobody caught until production data was wrong.

The developer who added the null check wasn't wrong to add it. They were working from incomplete information. They didn't know the original decision. They didn't know it was deliberate. Nobody told them. Nothing in the codebase told them. The comment in the code — if there had been one — might have helped. But the comment doesn't exist, because nobody wrote the ADR.


An Architectural Decision Record — ADR — is a document that records a single architectural decision: what was decided, why it was decided, what alternatives were explicitly considered and rejected, and what constraints future code must respect because of this choice.

The ADR for guest order storage looks like this:

ADR-012: Guest orders stored in unified orders collection

Status: Accepted

Context: Guest checkout requires order persistence without a user account.
Two options were considered: a separate guest_orders collection, or storing
guest orders in the existing orders collection with a nullable userId field.

Decision: Store guest orders in the unified orders collection with
userId: null and guestEmail: string.

Rationale:
- Operations requires a single workflow for order management (business constraint)
- Reporting and analytics queries run against a single collection; splitting
  creates dual-maintenance of every query
- Refund and returns logic already handles order ID + email lookups; no new
  code path required for guest returns

Rejected alternative: Separate guest_orders collection
Reason for rejection: Duplicates all order management logic; creates a
second surface for bugs; violates the single-workflow operations constraint.

Consequences:
- All order queries must handle userId == null explicitly
- Index on guestEmail required for returns lookup by email
- GDPR deletion requests must include guest order cleanup by email, not uid

The consequences section is where the ADR connects to the specific line of code the new developer was looking at.

When this ADR exists, that same consequence becomes a code comment:

Future<List<Order>> getOrdersByUser(String uid) async {
  // Guest orders (userId: null) are excluded by this filter. Do not add a
  // separate null check — see ADR-012 for the unified collection rationale.
  final snap = await _db.collection('orders')
      .where('userId', isEqualTo: uid)
      .orderBy('createdAt', descending: true)
      .get();
  return snap.docs.map(Order.fromDoc).toList();
}

The new developer reads that comment in week two. They don't add the null check. The analytics query works six months later. Three days of investigation never happen.


This is what ADRs are for. Not documentation for its own sake. Not a compliance artifact. A mechanism for making decisions made by past engineers available to future engineers — including future AI generation sessions — so those decisions don't have to be made again from incomplete information.

The cost of writing ADR-012: forty-five minutes, including the conversation where the team evaluated the two alternatives.

The cost of not writing it: three days of production investigation, six months later, plus however long it takes to fix the analytics query and verify that nothing else was affected.

The problem with ADRs is that the cost and the benefit are completely separated in time. The forty-five minutes happen now, during planning, when there's pressure to move fast and the benefit is invisible. The three days happen later, during a production incident, when the team has forgotten that the original decision was ever made, let alone that it was undocumented.

This is why ADRs are the most important file nobody writes. The value is real. The timing is terrible.


There are three common failure modes when teams try to adopt ADRs.

The first is writing them after the code is already committed. A post-hoc ADR is a rationalisation, not a decision record. It documents what was built, not what was decided — and critically, it doesn't capture the alternatives that were actually evaluated. If the guest_orders decision was made by the AI and documented afterward, the ADR might say "we used a unified collection because it was cleaner" rather than "we evaluated a separate collection and rejected it because of the operations single-workflow constraint." The first is an explanation. The second is a constraint on every future developer.

The second failure mode is treating the ADR as a design document — describing the full implementation rather than the single decision and its consequences. An ADR that explains how guest checkout works is five pages long and nobody reads it. An ADR that records one decision, the alternatives rejected, and the consequences for future code is one page and everyone references it.

The third failure mode is updating an accepted ADR when a decision changes. If the decision changes, the old ADR is marked superseded and a new one is written. The history of decisions is as valuable as the decisions themselves. "Why did we choose this over that?" is answerable from the ADR history. "Why did the decision change?" is answerable from the supersession trail. Rewriting the old ADR destroys both of those answers.


The most undervalued section of an ADR is the rejected alternatives.

Teams that write ADRs often treat the rejected alternatives section as optional, or fill it in with vague statements like "other approaches were considered." This defeats the purpose. The rejected alternatives section is what prevents a future developer — or a future AI — from independently arriving at the same alternative and implementing it without knowing it was already evaluated and ruled out.

ADR-012 explicitly rejects the guest_orders collection and explains why. When that ADR is in the AI's context window during Step 5 code generation, the AI cannot drift toward the separate collection. Not because it's programmed to avoid it, but because the document it's working from says the approach was evaluated, found to violate a specific constraint, and rejected. The AI generates the unified collection implementation. Every time. Without requiring anyone to re-explain the decision.

That's the power of the rejected alternatives section. It's not history. It's an active constraint on every future generation step.


The ADR feeds into later steps in ways that compound across the feature.

At Step 5, it constrains generation — the AI knows what decisions have been made and generates within them.

At Step 6, it guides verification — the developer checks that each ADR consequence is reflected in the implementation before the PR opens.

At Step 9, it enables review — the senior engineer reads the ADR first, then opens the diff, checking each consequence against the implementation. They're doing a conformance check, not a style check.

At Step 10, it closes the loop — if the implementation deviated from the ADR in any meaningful way, the ADR is amended to reflect what actually shipped.

One document. Written once. Referenced by every person who touches this feature, in every phase, for the lifetime of the codebase.

The new developer in week two doesn't add the null check. Not because they were told not to. Because the ADR told them why.


Comments

No comments yet. Be the first!

Sign in to leave a comment.