Architecture Decision Records: The Practice That Prevents Architectural Amnesia
Concept
Every system you inherit is a graveyard of unmade arguments. The database is PostgreSQL because three years ago someone liked it. The API is REST because "everyone knows REST." The event bus is Kafka because another team used it. None of these are necessarily wrong decisions — but they were made without documented reasoning, and now the team spends more time reverse-engineering intent than building features.
Architecture Decision Records (ADRs) were formalized by Michael Nygard in 2011. The concept is elegant in its simplicity: for every architecturally significant decision, write a short, structured document that captures the context, the decision, the alternatives considered, and the consequences. Store it alongside the code, in version control, as a first-class artifact of the project.
The key insight is that the why of a decision has a dramatically shorter half-life than the what. Code communicates what was built. Tests communicate what was expected. ADRs communicate why the team made the choices that constrain every future engineer on the project.
What Qualifies as Architecturally Significant?
Not every technical choice deserves an ADR. The threshold question is: will reversing this decision require coordinated effort across the team, or significant rework? If yes, document it.
Decisions that typically warrant ADRs:
- Choice of persistence technology (RDBMS vs. document store vs. event log)
- Service boundary definitions (where one bounded context ends and another begins)
- API protocol selection (REST vs. gRPC vs. GraphQL)
- Consistency model choice (strong vs. eventual) for a specific data set
- Authentication strategy (JWT vs. opaque token vs. session)
- Deployment topology (shared cluster vs. dedicated per-service)
- Third-party library lock-in (anything that's hard to swap out)
- Data retention and migration policy
Decisions that typically do not warrant ADRs:
- Implementation details within a service that don't cross team boundaries
- Library version bumps with no breaking changes
- Naming conventions (covered by a style guide)
- Formatting and linting choices (covered by tooling config)
The Nygard Format
The canonical Nygard ADR structure has four sections:
Title: A short noun-phrase identifying the decision. "Use PostgreSQL for Order Persistence." Not "Database Decision."
Context: The forces at play — technical constraints, team capabilities, regulatory requirements, time pressure, existing infrastructure. This section should be written as if explaining the situation to a capable engineer who was not in the room.
Decision: The decision itself, stated affirmatively. "We will use PostgreSQL 15 as the primary datastore for the Order service." Not wishy-washy. Committed.
Consequences: The full cost of the decision — good and bad. What becomes easier? What becomes harder? What are we now committed to? What did we rule out? This is the most valuable section and the most commonly skipped.
ADR Lifecycle States
ADRs are not static documents. They move through states:
- Proposed: Under discussion, not yet ratified
- Accepted: The decision is in effect
- Deprecated: Superseded by a newer ADR, but the historical record is preserved
- Superseded: Explicitly replaced — the new ADR references the old one
This lifecycle is critical. An ADR for "use REST for all internal APIs" written in 2019 should not be silently contradicted by a new gRPC service in 2024. It should be superseded, with the reasoning documented in the new ADR.
Constraints
The Discoverability Problem
ADRs stored in a docs/adr/ directory that no one knows about are marginally better than no ADRs at all. The practice requires three discoverability constraints to function:
- Location convention: The directory must be standardized across all repositories in the organization.
docs/adr/,adr/, or.adr/— pick one and enforce it. - Indexing: A
docs/adr/README.mdorindex.mdthat lists all ADRs with their current status and a one-line summary is not optional. Without it, the directory becomes a graveyard. - Onboarding integration: The ADR directory must be the first thing a new engineer reads, not the last. If it's in the onboarding checklist as "optional reading," it will never be read.
The Currency Problem
An ADR written in 2020 for a system that has evolved significantly is not just stale — it's actively misleading. This is worse than no documentation, because it creates false confidence. The practice requires a periodic review cadence (quarterly for active systems, annually for stable ones) where a designated "architecture steward" reviews ADRs for relevance and triggers deprecation or supersession.
The Scope Problem
ADRs work at the service or bounded-context level. They do not naturally aggregate across an organization of 30 services. Cross-cutting concerns (authentication strategy, event bus selection, telemetry stack) need a separate layer: organization-level ADRs owned by the architecture guild or CTO's office. Without this distinction, teams write service-level ADRs that silently contradict each other.
Trade-offs
Lightweight vs. Heavyweight Format
The Nygard format is intentionally minimal. Alternatives exist:
MADR (Markdown Architectural Decision Records) adds fields: decision drivers, considered options, pros/cons per option, links to related ADRs. It's more comprehensive and produces better onboarding material, but costs 3–4x more time per record.
RFC format (Request for Comments) — used by large organizations like Rust, Kubernetes — is a full proposal document with stakeholder review. It catches more edge cases but creates a bureaucratic bottleneck that discourages writing.
The right choice depends on team size and decision stakes:
| Team Size | Decision Stakes | Recommended Format |
|---|---|---|
| < 10 engineers | Single-service scope | Nygard (minimal) |
| 10–50 engineers | Multi-service scope | MADR |
| 50+ engineers | Org-wide standards | RFC |
Code-adjacent vs. Centralized Wiki
Storing ADRs in the repository alongside code has the advantage of version co-location — you can git log a file and see exactly which ADR was in effect when a specific commit was made. The disadvantage is that cross-service ADRs are not naturally discoverable from a central location.
The recommended hybrid: service-level ADRs live in the repository; organization-level ADRs live in a dedicated architecture repository with its own ADR index. A CI check validates that every service repository has a non-empty ADR index.
Automation and Tooling
The adr-tools CLI (by Nat Pryce) automates ADR creation, status management, and index generation. For .NET teams, the pattern fits naturally into a repository template with a scripts/new-adr.sh wrapper. The ADR index can be generated as part of CI and published to an internal docs portal.
Code
The following shows a .NET project structure convention enforcer — a build-time MSBuild task that warns when a project has no ADR index, making the absence of ADRs a visible build artifact:
// AdrIndexValidatorTask.cs
// MSBuild custom task that validates ADR index exists and is non-empty
// Add to a shared build props file imported by all projects
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using System.IO;
using System.Linq;
public class ValidateAdrIndexTask : Task
{
[Required]
public string ProjectDirectory { get; set; } = string.Empty;
public string AdrRelativePath { get; set; } = "docs/adr";
public override bool Execute()
{
var adrDirectory = Path.Combine(ProjectDirectory, AdrRelativePath);
if (!Directory.Exists(adrDirectory))
{
Log.LogWarning(
subcategory: "ADR",
warningCode: "ADR001",
helpKeyword: null,
file: ProjectDirectory,
lineNumber: 0,
columnNumber: 0,
endLineNumber: 0,
endColumnNumber: 0,
message: "No ADR directory found at {0}. " +
"Architecturally significant decisions should be documented. " +
"Run 'scripts/new-adr.sh' to create your first record.",
messageArgs: adrDirectory);
return true; // warn, don't fail the build
}
var adrFiles = Directory
.GetFiles(adrDirectory, "*.md", SearchOption.TopDirectoryOnly)
.Where(f => !f.EndsWith("README.md", StringComparison.OrdinalIgnoreCase))
.ToArray();
if (adrFiles.Length == 0)
{
Log.LogWarning("ADR", "ADR002", null, ProjectDirectory,
0, 0, 0, 0,
"ADR directory exists but contains no decision records. " +
"Document at least one architectural decision.",
null);
}
else
{
Log.LogMessage(MessageImportance.Normal,
$"ADR validation passed — {adrFiles.Length} decision record(s) found.");
}
return true;
}
}
The second example shows a concrete ADR for a real decision: choosing an outbox pattern for reliable event publishing. This is the format senior engineers should produce:
// adr-0012-outbox-pattern-for-event-publishing.md (represented as a C# doc comment
// template to illustrate the decision embedded near the implementation)
// DECISION RECORD ADR-0012
// Title: Use Transactional Outbox Pattern for Domain Event Publishing
// Date: 2024-03-05
// Status: Accepted
// Deciders: Platform Team, Order Domain Team
//
// Context:
// The Order service must publish OrderPlaced events to the event bus (RabbitMQ)
// after a successful database write. Direct publish-after-save risks dual-write
// failure: the DB write succeeds but the publish fails, leaving downstream
// consumers with a stale view. Conversely, publish-then-save risks phantom events.
//
// Decision:
// We will implement the Transactional Outbox pattern. Domain events are written
// to an `outbox_events` table in the same DB transaction as the aggregate state.
// A background relay process polls the outbox and publishes to RabbitMQ,
// marking records as published on success. This guarantees at-least-once delivery.
//
// Consequences:
// + Eliminates dual-write failure between DB and message broker.
// + Events survive broker downtime — they persist in the outbox until delivered.
// - Introduces eventual consistency (typically < 500ms relay lag).
// - Consumers must be idempotent (at-least-once delivery).
// - Adds outbox relay infrastructure to operational surface area.
// - Cross-service saga correlation requires idempotency key propagation.
//
// Alternatives considered:
// 1. Direct publish after save — rejected: dual-write failure risk.
// 2. Two-phase commit (XA) — rejected: not supported by RabbitMQ, high overhead.
// 3. Change Data Capture (Debezium) — deferred: operational complexity too high
// for team's current platform maturity. Revisit in ADR-0020.
public class OutboxRelayService : BackgroundService
{
private readonly IOutboxRepository _outboxRepository;
private readonly IMessagePublisher _publisher;
private readonly ILogger<OutboxRelayService> _logger;
private readonly TimeSpan _pollingInterval = TimeSpan.FromMilliseconds(250);
public OutboxRelayService(
IOutboxRepository outboxRepository,
IMessagePublisher publisher,
ILogger<OutboxRelayService> logger)
{
_outboxRepository = outboxRepository;
_publisher = publisher;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
var pendingEvents = await _outboxRepository
.GetPendingAsync(batchSize: 50, stoppingToken);
foreach (var outboxEvent in pendingEvents)
{
await _publisher.PublishAsync(outboxEvent.EventType,
outboxEvent.Payload,
stoppingToken);
await _outboxRepository.MarkPublishedAsync(
outboxEvent.Id, stoppingToken);
_logger.LogDebug("Relayed outbox event {EventId} of type {EventType}",
outboxEvent.Id, outboxEvent.EventType);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Outbox relay cycle failed — will retry in {Interval}",
_pollingInterval);
}
await Task.Delay(_pollingInterval, stoppingToken);
}
}
}
Further Reading
- Module 16 – Governance & Architecture Decision Making — the organizational process for ratifying and reviewing ADRs
- Module 11 – Saga Pattern & Distributed Transactions — ADRs for consistency model decisions in distributed systems
- Module 3 – Distributed Systems Fundamentals — why dual-write problems require explicit architectural decisions
- Module 10 – C4 Model & Architecture Documentation — integrating ADRs with C4 diagrams for complete documentation
External references:
- Nygard, M. (2011). "Documenting Architecture Decisions." Cognitect Blog.
- Keeling, M. (2017). Design It! From Programmer to Software Architect. Pragmatic Bookshelf.
- Pryce, N.
adr-toolsCLI: https://github.com/npryce/adr-tools