Writing ADRs: Decision Records with the Nygard Template, MADR 4.0 and Y-Statements
A good ADR records not just the decision but the question it answers, the options it was made against, the accepted cost and when to reopen it, and it is never edited once decided. Nygard's five-section template, the sections and front matter of MADR 4.0.0, a rejected decision record, Y-statements, the status lifecycle and a real script that enforces immutability in PRs.
Version note: The code in this post was built and run with C# 14 and .NET 10.
Guess First
Bookstore's modular monolith decision was made in March. The record was written in July, when a new developer asked "is there any documentation for this structure?". This is the whole of docs/decisions/0003-modular-monolith.md in the repository:
# Modular monolith
We use a modular monolith. Each module lives in src/Modules and has its own
DbContext. Modules talk to each other through public interfaces.
Every sentence is true. What can someone who joins the team a year from now not learn from this file? How many questions can you count? And one more: is this a decision record, or a description of the current structure?
Nygard's Template: Five Sections
The file above tells the "what", not the "why". It does not say which problem was solved, which options were discussed, which cost was accepted, who decided and when the decision should be reopened. Michael Nygard's 2011 post "Documenting Architecture Decisions" fills that gap with five sections: Title, Context, Decision, Status and Consequences. Bookstore's e-mail decision, the one made in the Introduction to Architecture post "Trade-off Thinking", looks like this in that template:
# 4. Order e-mail through an in-process queue
Date: 2026-03-19
## Status
Accepted
## Context
Checkout sends the confirmation e-mail inside the request. When the e-mail
provider is slow, checkout takes as long as the provider; when it is down,
checkout fails and customers retry, which creates duplicate orders. Checkout
availability and latency are in our top three characteristics. We have one
part-time ops engineer and no message broker.
## Decision
We will put the confirmation e-mail on a bounded in-process queue
(System.Threading.Channels) and send it from a background worker that retries
until the provider is back.
## Consequences
Checkout no longer depends on the e-mail provider. E-mails still in the queue
are lost when the process stops (deploy, crash); customers can see the order
on the order page and support can resend the e-mail. We will log how many
messages are left in the queue at shutdown and revisit this decision when
more than 10 e-mails a month are lost or another module needs order events.
The Title is numbered and short. Context describes the forces behind the decision in neutral language: technical, team and project conditions, together with the tension between them. Decision starts with an active sentence, as Nygard suggests: "We will…". Status says where the decision stands. Consequences writes down not only the good outcomes but also the accepted cost and the condition for revisiting the decision; that last sentence is what prevents the "blind reversal" scenario from the first post.
Nygard has two more rules. A record does not exceed one or two pages; if it does, it either contains more than one decision or has turned into a design document. Records are numbered sequentially and a number is never reused. Nygard suggests keeping the files in the repository under doc/arch/adr-NNN.md. This format is exactly what Nat Pryce's adr-tools command-line tool produces; the file above has the same structure as an ADR (Architecture Decision Record) created with adr-tools.
We write the records in English, because Bookstore's repository language is English and the team in the second country will read these files too. For a team that writes in Turkish the template does not change; what matters is that the whole decision log is in one language.
MADR 4.0: Sections and Front Matter
Nygard's template is short, but it does not ask for the options as a separate section; alternatives, if any, get lost inside Context. MADR (Markdown Architectural Decision Records) closes that gap. The current version of the template, which Oliver Kopp, Anita Armbruster and Olaf Zimmermann introduced in a 2018 paper, is 4.0.0 from 17 September 2024, as we checked on adr.github.io at the time of writing. Bookstore's modular monolith decision, as the opening file should have been:
---
status: accepted
date: 2026-03-12
decision-makers: Ada (tech lead), Alan, Grace
consulted: Linus (ops)
informed: product team
---
# Start with a modular monolith
## Context and Problem Statement
Bookstore is one ASP.NET Core application with one SQL Server database,
built by three developers and a part-time ops engineer who deploy weekly.
The 18-month plan expects three times the order volume, a second country
and four stream-aligned teams. Catalog, Ordering, Inventory, Payment,
Shipping and Notification exist as namespaces, but nothing stops Ordering
from reading Catalog tables directly. How do we structure the system so we
can ship weekly today and split the work across teams later?
## Decision Drivers
* Modifiability: a new payment method takes at most 3 developer-days and changes only Payment
* Availability: checkout is available 99.9% of the month
* One team and one part-time ops engineer; no platform team yet
* Weekly deploys stay a single pipeline
## Considered Options
* Layered monolith (today's structure)
* Modular monolith
* Microservices, one service per module
## Decision Outcome
Chosen option: "Modular monolith", because it is the only option that gives
the module boundaries our modifiability scenario needs without adding network
calls, separate deployments and an operational load that one part-time ops
engineer cannot carry.
### Consequences
* Good, because module boundaries become explicit and testable before the teams split.
* Good, because there is still one deployment, one database server and one pipeline.
* Bad, because a memory leak in one module can still take the whole process down.
* Bad, because boundaries inside one process erode easily and need automated checks.
### Confirmation
An architecture test (ArchUnitNET) in CI fails when a module uses another
module's internal types. The monthly architecture review checks new
cross-module calls.
## Pros and Cons of the Options
### Layered monolith (today's structure)
* Good, because there is no migration work.
* Bad, because a payment change touches the Web, Business and Data layers.
* Bad, because nothing shows which code will belong to which team.
### Modular monolith
* Good, because each module owns its tables and exposes a public contract.
* Neutral, because the database server stays shared; each module gets its own schema.
* Bad, because moving code into modules costs about two sprints.
### Microservices, one service per module
* Good, because modules could be deployed and scaled independently.
* Bad, because independent deployability is explicitly a "not now" characteristic.
* Bad, because tracing, retries and on-call for six services exceed our ops capacity.
## More Information
Revisit when a second team needs its own release cadence, or when one module
must scale independently during campaigns. Related: ADR-0002 (SQL Server as
the single database), ADR-0004 (order e-mail through an in-process queue).
The YAML (YAML Ain't Markup Language) front matter at the top separates the decision's metadata from its body. status and date say where the decision stands and when it was last updated. The decision-makers, consulted and informed fields answer the "who owns the decision?" question from the first post; consulted are the experts with two-way communication, informed are the people who are only kept up to date. The deciders field from version 3.0 was renamed to decision-makers in 4.0.
The body has three mandatory sections: Context and Problem Statement, Considered Options and Decision Outcome. Ending the problem statement with a question is a good habit; the reader knows which question they are reading the answer to. Decision Drivers are the forces used to weigh the options; here we used the measurable scenarios from the Introduction to Architecture series, not wishes like "it should be flexible". Decision Outcome gives the option and its justification in one sentence, Consequences writes down the cost.
The most important change in 4.0 is that the old Validation section was renamed Confirmation and moved under Decision Outcome. This section asks "how will we know the decision was implemented?". The answer can be an architecture test, a CI (Continuous Integration) check or a review step. The sixth post of this series is entirely about turning this section into an automated check. Pros and Cons of the Options evaluates every option with "Good, because" and "Bad, because"; using the same pattern in Consequences is a deliberate MADR choice, so the reader can read both sections the same way. More Information holds the condition for reopening the decision and the related ADRs.
A suggestion from Martin Fowler's ADR post of March 2026 helps here: write the record like a news story, in inverted pyramid order; the most important information at the top, the details below. MADR's order fits that: first the question, then the chosen option, and at the end the detailed pros and cons of the options. Fowler and Microsoft's Well-Architected documentation suggest one more thing: record the decision's confidence level. Marking a decision made with little information as "low confidence" legitimizes reopening it at the first opportunity. MADR has no separate field for this; one sentence in More Information is enough.
A Rejected Decision Is a Record Too
In the first post we said the "should we move to PostgreSQL?" debate gets reopened with every new developer. The Bookstore team had that debate in May and decided to stay on SQL (Structured Query Language) Server. The record:
---
status: rejected
date: 2026-05-06
decision-makers: Ada, Grace
consulted: Linus (ops), Alan
informed: all developers
---
# Use PostgreSQL instead of SQL Server
## Context and Problem Statement
The second country needs its own production database, and SQL Server
licensing for one more environment is a new cost. Two developers have used
PostgreSQL before. Should Bookstore move from SQL Server to PostgreSQL before
the second country goes live?
## Decision Drivers
* Licensing cost of one more production environment
* 41 EF Core migrations, three of them with SQL Server specific SQL (rowversion, filtered indexes, full-text catalog)
* Linus knows SQL Server backups, monitoring and failover; nobody has run PostgreSQL in production
* The second country goes live in four months
## Considered Options
* Move to PostgreSQL now
* Stay on SQL Server
* PostgreSQL only for new modules
## Decision Outcome
Chosen option: "Stay on SQL Server", because moving the database four months
before a launch puts the launch at risk for a cost saving that is smaller
than the migration work, and running two database engines doubles the
operational knowledge we need.
### Consequences
* Good, because the launch does not depend on a database migration.
* Bad, because we pay for one more SQL Server environment.
## Pros and Cons of the Options
### Move to PostgreSQL now
* Good, because there is no license cost for new environments.
* Bad, because the full-text catalog search has to be rewritten and tested again.
* Bad, because nobody on the team has operated PostgreSQL in production.
### Stay on SQL Server
* Good, because operations, backups and monitoring stay as they are.
* Bad, because licensing cost grows with every environment.
### PostgreSQL only for new modules
* Good, because new modules could try it with low risk.
* Bad, because two engines mean two backup, monitoring and upgrade processes.
## More Information
Reopen when a third production environment is planned, or when the team has
someone with PostgreSQL operations experience.
The status is rejected: the proposed change was not accepted. The title is the proposal itself ("Use PostgreSQL instead of SQL Server"), while the chosen option keeps the current state. It may look odd at first, but someone searching the decision log for "PostgreSQL" finds this record and sees the outcome of the debate, its rationale and the condition for reopening it in one file. Why Bookstore stays on SQL Server in all the other series is now written down in a file too.
Y-Statements: The Decision in One Sentence
Sometimes you need a summary of a decision: in a PR (pull request) description, in a release note, in the index of the decision log. The Y-statement, which Olaf Zimmermann proposed in 2012 at the SATURN (SEI Architecture Technology User Network) conference of the SEI (Software Engineering Institute), fits a decision into one sentence with six parts: context, concern, chosen option, rejected options, the quality gained and the cost accepted. The Y-statement of ADR-0003:
In the context of a growing Bookstore that will be split across four teams,
facing modules that can freely read each other's tables,
we decided for a modular monolith
and neglected the layered monolith and microservices,
to achieve module boundaries that we can test before the teams split,
accepting that one module's failure can still take the whole process down
and that the boundaries need automated checks.
The sentence is long, but demanding: if you cannot write the "to achieve" part, you do not know which quality the decision aims for; if you cannot write the "accepting that" part, you do not see the cost. Zimmermann also suggests splitting it into two or three sentences for readers who find it hard to read, and adding a "because" at the end when needed. Some teams use the Y-statement as a record format on its own; we use it as the first paragraph of a MADR record or as the summary in the index.
Status Lifecycle and Immutability
MADR's status list is: proposed, rejected, accepted, deprecated and superseded by ADR-NNNN. An ADR is born as proposed; if accepted in review it becomes accepted, otherwise rejected. When an accepted decision loses its validity over time there are two paths. If a new decision replaced it, the old record points to the new one with superseded by. If it became invalid with nothing replacing it, for example because a module was removed entirely, it becomes deprecated.
The most important rule: the body of a decided ADR does not change. Every source, from Nygard's post to Fowler's 2026 post and Microsoft's documentation, says the same: if you want to change the decision, write a new record and supersede the old one. Why? Because the value of the decision log is in its history. In March an in-process queue was chosen, in October the team moved to an outbox; if these two pieces of information stand separately, someone six months later can read both what was chosen and why, and what changed and why. If the old record is overwritten, the March rationale and the condition under which it stopped holding are lost. Edits that do not change the meaning, such as fixing a typo or a broken link, can count as an exception to this rule; but the team has to agree on that explicitly.
For file names MADR suggests docs/decisions/NNNN-title-with-dashes.md: a four-digit sequential number and a lowercase title with dashes. The number makes file system order chronological, and it assumes a repository will not reach 10,000 records. The title should state the decision itself, not its topic: not 0003-modular-monolith.md but 0003-start-with-a-modular-monolith.md.
Reviewing an ADR as a Pull Request
Because an ADR lives in the same repository as the code, as Markdown, we can review it the same way we review code. A decision is opened as a pull request with status proposed, comments are collected in the PR, once approval comes the status is set to accepted and the PR is merged. The moment of the merge is the decision's date. The CODEOWNERS file we saw in the Git and GitHub series makes sure decision records are not merged without approval from the right people:
# Decision records are not merged without approval from at least one tech lead
/docs/decisions/ @bookstore-org/tech-leads
Instead of leaving the immutability rule to human attention, we check it with a small script. For every ADR changed in the PR, the script looks at its status on the base branch; if it is not proposed, it allows only the status and date lines to change and rejects deleting the file:
#!/usr/bin/env bash
# The body of a decided ADR never changes; only the status and date lines may be updated
set -euo pipefail
base="${1:-origin/main}"
failed=0
while IFS=$'\t' read -r change path; do
[[ "$change" == "A" ]] && continue
old_status=$(git show "$base:$path" | sed -n 's/^status: *"\{0,1\}\([a-z]*\).*/\1/p' | head -n 1)
[[ "$old_status" == "proposed" ]] && continue
if [[ "$change" == "D" ]]; then
echo "::error file=$path::ADR is '$old_status'; do not delete it, mark it deprecated or superseded"
failed=1
continue
fi
body_changes=$(git diff -U0 "$base"...HEAD -- "$path" \
| grep -E '^[+-]' | grep -vE '^(\+\+\+|---)' | grep -vE '^[+-](status|date):' || true)
if [[ -n "$body_changes" ]]; then
echo "::error file=$path::ADR is '$old_status'; do not edit it, write a new ADR that supersedes it"
echo "$body_changes" | head -n 4
failed=1
fi
done < <(git diff --name-status --no-renames "$base"...HEAD -- docs/decisions)
[[ $failed -eq 0 ]] && echo "ADR immutability check passed"
exit $failed
In October Alan opened a PR that moved the order e-mail to an outbox and, "so the docs are up to date", changed the Decision Outcome line of ADR-0004. The script's real output in a local repository:
::error file=docs/decisions/0004-order-email-through-an-in-process-queue.md::ADR is 'accepted'; do not edit it, write a new ADR that supersedes it
-Chosen option: "In-process queue", because it removes the e-mail provider
+Chosen option: "Transactional outbox", because it removes the e-mail provider
exit code: 1
In another PR someone wanted to "clean up" by deleting the outdated-looking ADR-0004:
::error file=docs/decisions/0004-order-email-through-an-in-process-queue.md::ADR is 'accepted'; do not delete it, mark it deprecated or superseded
exit code: 1
The right path: open a new ADR-0011 and set the status of ADR-0004 to superseded by ADR-0011. In the diff, one line changes in the old record:
.../decisions/0004-order-email-through-an-in-process-queue.md | 2 +-
.../0011-order-events-through-a-transactional-outbox.md | 11 +++++++++++
2 files changed, 12 insertions(+), 1 deletion(-)
ADR immutability check passed
exit code: 0
Below is the GitHub Actions workflow that runs the script on every PR. We ran the script and the three scenarios locally; we did not run the workflow on GitHub for this post. The ::error file=…:: lines are GitHub Actions' workflow command format; in the PR they show up as an error annotation on the file.
name: adr-check
on:
pull_request:
paths: ["docs/decisions/**"]
jobs:
immutability:
runs-on: ubuntu-latest
steps:
# The full history is needed to compare against the base branch
- uses: actions/checkout@v7
with:
fetch-depth: 0
- run: ./tools/check-adr-immutability.sh "origin/${{ github.base_ref }}"
What should a review look at? Our checklist is short: is the problem statement a question, are there at least two real options (one can be "do nothing"), does every option have at least one "Bad, because", does Consequences state the cost, does Confirmation name a checking mechanism, and does More Information contain a condition for reopening. If all the "Good" points sit with the chosen option and all the "Bad" points with the others, the decision was probably made first and the record justified afterwards.
Common Pitfalls
Writing the ADR months after the decision. The opening file is the result: the March debate is not remembered in July, and only a description of the structure remains. Open the ADR in the same PR as the decision, or before the decision as proposed.
Writing the outcome without showing the options. "We use a modular monolith" is not a decision, it is a state. Without Considered Options and Pros and Cons the reader cannot know what the decision was made against, and has the same debate again.
Fake options. Listing an option nobody seriously considered just to fill the list lowers the record's credibility. The options should be the ones that were really on the table.
Updating an accepted ADR. Fixing the old record because circumstances changed erases the history. Write a new record and supersede the old one; the script above enforces this mechanically.
More than one decision in a file. "Modular monolith and a schema per module and events between modules" is three decisions; when one changes, the record of the other two becomes unclear too. One decision, one file.
Turning the ADR into a design document. Class diagrams, endpoint lists and migration steps do not belong in an ADR. Microsoft's advice is clear: link to supporting material, but the decision must be understandable without it.
A Small Experiment
★☆☆ Rewrite the three-line opening file with the MADR 4.0 template and add its Y-statement at the end. Which section was hardest to write? The hardest section may be the topic the team discussed least for that decision.
★★☆ Add two rules to check-adr-immutability.sh: fail when the file name does not match NNNN-title-with-dashes.md and when the status field is not in MADR's list. Run the three scenarios again in your own small repository.
★★★ Pick a decision in your team that was made but never recorded, talk to the people who made it for 30 minutes and write a retroactive ADR. Make the status accepted and the date the real decision date; in More Information note that the record was written afterwards and with whom. Which piece of information does nobody remember anymore?
The options in this post were small and familiar; the Pros and Cons filled up with a few points. In the next post we move to a harder situation: how to weigh options against prioritized scenarios when their pros and cons are disputed, the sensitivity and trade-off point concepts from ATAM (Architecture Tradeoff Analysis Method), and a spike we ran to add a cache to the Bookstore catalog.