Topic: adr numbering scheme
ADR Numbering Scheme — Padding, Gaps, and Collision Recovery
The templates tell you to number your ADRs. They don't tell you how many digits, what to do about gaps, how to handle two PRs racing for the same number, or whether to renumber when the directory looks messy. Here are the operational answers — and the merge-base allocator that makes the racing-PR problem disappear.
TL;DR
Use 4-digit zero-padded IDs (0001-stack-postgres.md). Allocate monotonically against the merge-base of HEAD with origin/main — not against HEAD itself — so two PRs opened in parallel each pick up the number they would have gotten on a fresh branch and the second to land has to bump on rebase. Gaps are normal: a closed-without-merge PR's number is lost forever, and that's correct. Never renumber existing ADRs — the number is part of the supersession pointer, and renumbering breaks every link that referenced the old slug. The CI job in the ADR GitHub Action workflow implements the merge-base allocator and catches collisions before they merge.
Why numbering matters more than it looks like it should
An ADR's number is not a sort key. It's a permanent identifier that other ADRs link to, that PR descriptions reference, that code comments cite, and that becomes the key in any external reference (an architecture diagram label, a runbook citation, a Slack thread). The moment you treat numbers as cosmetic and renumber to "clean up gaps," every external reference becomes a broken link to a different decision than the reader expected. The templates don't make this rule obvious because the templates are about content shape, not directory operations — but every long-lived ADR directory eventually faces the numbering questions and the wrong answer corrupts the audit trail in a way that's expensive to recover.
This page is the operational complement to the Nygard template, the markdown template, and MADR — those tell you the body shape; this tells you how to allocate, version, and never renumber the file that holds it.
How many digits — 3, 4, or 5?
Four. Not three. Not five. The argument is short.
| Padding | Range | When you'll regret it |
|---|---|---|
001 (3-digit) | 1–999 | Around year 5 on a fast-moving team. npryce/adr-tools shipped 3-digit by default and large adopters routinely run out — the migration to 4-digit then has to renumber 999 files and rewrite every supersession pointer, which is exactly the operation this page tells you never to do. Skip 3-digit unless you're certain you'll never cross 999. |
0001 (4-digit) | 1–9,999 | Approximately never. Even the largest engineering organizations don't write 9,999 ADRs in any practical timeframe. Linux kernel architecture has fewer than 200 documented decisions in 30 years; a 200-engineer SaaS company hits ~50/year at peak. 4-digit is the universal default for a reason. |
00001 (5-digit) | 1–99,999 | Approximately never, but with extra characters that make filenames harder to scan. The trade-off is asymmetric: you get nothing for the extra digit and pay it on every ls for the lifetime of the directory. |
Filename shape: NNNN-kebab-case-slug.md. The slug is for humans (so ls output is readable); the number is for tools (so the filenames sort lexicographically and so the supersession pointers can name the file by stable ID). Both are required. A directory of bare 0001.md files becomes unreadable past about ten ADRs; a directory of bare pick-postgres.md files breaks every supersession pointer the first time someone renames a slug.
Gaps are normal — don't close them
The numbering sequence is monotonic with gaps allowed. A PR that opens with the next-available number (say ADR-0042) and is closed without merging takes that number with it. The next ADR PR allocates ADR-0043; ADR-0042 is gone forever. Reusing the number for a different decision later is the wrong move because:
- Git history references it. The PR description, the closing comment, the discussion threads, and any commit that mentioned "see ADR-0042" all now point at a decision that isn't there. A reader following the citation finds an unrelated ADR with the same number and gets confused.
- External links reference it. Slack threads, runbooks, architecture-diagram labels, and onboarding docs that named ADR-0042 don't get updated when you renumber. Reuse poisons every external reference.
- The CI integrity check assumes uniqueness. The supersession-pointer check in adr-github-action resolves
Supersedes: 0042-foo.mdby exact filename match. Reusing 0042 with a different slug doesn't break the check (different filename) but does silently change which decision a reader thinks was superseded.
Gaps are visible (a directory that goes 0040, 0041, 0043, 0044) but harmless — they tell the reader "a decision was considered and not adopted at this point in time," which is itself useful information. The instinct to "tidy" the sequence is the same instinct that makes new teams renumber files when refactoring; resist it.
The merge-base-aware allocator
The interesting failure mode is two PRs racing for the same number. Engineer A opens a PR adding 0042-pick-bullmq.md; engineer B, working in parallel on a separate branch, opens a PR adding 0042-rate-limit-strategy.md. Neither has rebased recently. Both PRs pass local CI (because at the time of branch creation, 0042 was the correct next number). The second to merge silently overwrites the first if the allocator naively reads HEAD; or the second's PR fails late in the review cycle if the allocator only checks at merge time.
The fix is to allocate against the merge-base, not HEAD:
#!/usr/bin/env bash
# Compute the next ADR number relative to where this branch diverged from main.
# This is what a fresh branch off main would compute right now.
set -euo pipefail
DIR="${1:-doc/decisions}"
# Find the merge-base of HEAD with origin/main.
mb=$(git merge-base HEAD origin/main)
# List ADRs that exist at the merge-base (not at HEAD).
existing_at_mb=$(git ls-tree --name-only "$mb" -- "$DIR" \
| grep -E '^[0-9]{4}-' | sort)
# The highest number at merge-base + 1 is the allocator's answer.
last=$(echo "$existing_at_mb" | tail -1 | grep -oE '^[0-9]{4}' || echo "0000")
next=$(printf "%04d" $((10#$last + 1)))
echo "$next"
Two PRs running this at branch-open time get the same answer (say 0042). The first to merge lands its 0042-foo.md. The second runs the allocator again on its CI rebase; git ls-tree at the new merge-base now includes 0042-foo.md, so the allocator returns 0043. The CI job catches the mismatch — the file is named 0042-bar.md but the allocator says 0043 — and fails the PR until the second engineer renames the file. No silent overwrites, no late surprises.
This is the second job in the four-job ADR GitHub Action workflow. The full workflow runs four checks (structure-lint, numbering, supersession integrity, auto-index rebuild) on every PR and on a nightly schedule.
Forking and squashing — what happens to numbers
Two operations look like they should renumber but shouldn't.
Squashing a stack of ADR-PRs
If a single feature branch added three ADRs across three commits, squash-merging into main is fine — the file shapes don't change, the numbers don't shift, and the supersession pointers remain valid. The only thing that changes is the git log shows one combined commit instead of three; the directory state is identical to merging the three PRs in order. Don't try to renumber to "0040, 0041, 0042" if the squash put them all in the same commit; if the original branch picked 0040, 0042, and 0044 (because someone else's PR landed 0041 and 0043 between the original branch's commits), keep them as 0040, 0042, 0044. The gaps tell the truth.
Forking a repo with an ADR directory
If you fork a project and want to add your own decisions, do not renumber the upstream ADRs. The two reasonable schemes:
- Continue the upstream sequence. If upstream's last ADR is 0017, your first new one is 0018. Acceptable when your fork stays close to upstream and you may eventually upstream the new decisions. Easy supersession across the fork boundary.
- Use a fork-prefix. Your new ADRs are
F0001-...,F0002-..., etc. Acceptable when you've diverged sharply and don't intend to upstream. Clear visual marker inlsoutput. Supersession across the boundary is awkward but rare.
Either is fine. Renumbering upstream's ADRs is not — it breaks the upstream's audit trail at the next merge and signals to anyone reading the fork that the team treats numbers as mutable, which undermines confidence in the rest of the directory's integrity.
The "I really need to renumber" scenarios — and why none of them are good enough
Three motivations come up. None survives a careful reading.
- "My directory has gaps and looks messy." Aesthetic only. The cost is breaking every external reference; the benefit is none. Live with the gaps.
- "I migrated from 3-digit to 4-digit padding." Don't renumber. Add a one-line script that left-pads existing 3-digit filenames with a leading zero (
017becomes0017) — the supersession pointers update with a single sed across the directory because the slug suffix is unchanged. The numbers themselves don't shift. Padding migration is a rename, not a renumber. - "I imported ADRs from another team and want to merge sequences." Use a prefix (
T1-0017-...) for the imported set, or copy the import into a separatedoc/decisions/imported/subdirectory. Merging two sequences into a single one means picking which side's numbers shift; whichever side shifts loses every existing reference. Keep them separate, or accept the cost knowingly.
What goes wrong if you don't have CI numbering
Three failures show up in directories without an allocator running on every PR. (1) Silent collisions. Two engineers, both using ls doc/decisions/ | tail -1 as their allocator, both pick the same number. The second to merge overwrites the first because git happily fast-forwards when the file paths don't conflict by content (different slugs, same number prefix means actually different filenames — git sees them as two separate adds, both succeed, both files exist with the same number). The CI structure-lint catches it; the absence of CI does not. (2) Stale numbers. An engineer branches at week 1, opens the PR at week 4, never rebases. The PR claims ADR-0042; meanwhile mainline has marched to 0050. Without a merge-base allocator the PR merges with a stale number that's already taken. (3) Renumbering temptation. Without CI, the directory drifts inconsistent (mixed padding, occasional duplicates, occasional gaps that look like errors), and someone "fixes" it with a renumber. Every link breaks. The CI is what protects against the temptation to clean up, because the CI's invariant is "numbers don't change" rather than "numbers are dense." Mechanical enforcement keeps human judgment from corrupting the audit trail.
How WhyChose fits in
This page is about the directory operations that keep the ADR audit trail honest after a decision lands. The harder problem upstream is the decisions that never reach the directory — the architecture calls made in a ChatGPT or Claude tab whose rationale walks off in someone's chat history. The WhyChose extractor reads your AI chat exports, surfaces every decision-shaped exchange with its trade-offs, and outputs structured records you can promote into ADRs in your repo. The extractor doesn't allocate numbers — it produces decision-shaped Markdown blobs, and your CI workflow's allocator picks the next ID when each one becomes a PR. Together: the extractor closes the gap between chats and the directory; the merge-base allocator keeps the directory consistent under parallel writes; and the supersession protocol at how to update an ADR keeps it consistent over time.
Related questions
Should I use 3-digit, 4-digit, or 5-digit padding?
4-digit. 3-digit (001-999) caps at one ADR every two weeks for forty years before you collide; that's plenty for a small team but feels constrained on enterprise teams that hit 200 ADRs in five years. 5-digit (00001) is over-specified and makes filenames longer than they need to be without buying anything until you're past 9,999. 4-digit padded (0001-9999) is the modal choice and what nearly every published ADR template defaults to. Pick 4 digits and stop thinking about it.
Are gaps in the numbering OK?
Yes — gaps are normal and you should not try to close them. ADR numbers are assigned at PR-open time; if a PR is closed without merging, the number it claimed is gone. Reusing the number for a different decision is the wrong move because git history, PR comments, and any external link that references ADR-0042 now points at a decision that's not the original. Treat the sequence as monotonic with gaps allowed.
What happens when two PRs both grab ADR-0042 at the same time?
The first to land wins; the second has to renumber. The merge-base-aware allocator computes the next ADR number from the merge-base of HEAD with origin/main, not from HEAD itself — so a PR opened against a branch that's three days behind main still picks up the number it would have gotten on a fresh branch. When the second PR rebases, the allocator detects that ADR-0042 already exists at HEAD and bumps the new file to ADR-0043. The CI job catches the mismatch and fails the PR until the file is renamed.
Can I renumber ADRs to clean up the gaps?
No — renumbering is the most expensive operation you can do to an ADR directory and it buys nothing. The number is part of the supersession pointer (Supersedes: 0017-pick-bullmq-as-job-queue.md), which means renumbering ADR-0017 to ADR-0014 breaks every Supersedes / Superseded-by line that named the old slug, every PR comment that linked to the file, every external bookmark, and every reference in code comments. Gaps are the cost of immutability; pay them gladly.
Does the slug also need to be stable?
Yes — the slug is the second half of the supersession pointer. The pointer format is NNNN-slug.md, and supersession integrity checks resolve targets by exact filename match. Renaming a slug is the same operation as renumbering: it breaks every back-pointer and every external link. If a slug was poorly chosen, the right move is to write a new ADR with the corrected framing and supersede the original — leaving both files in place.
Further reading
- The ADR GitHub Action workflow — the four-job CI workflow that implements the merge-base allocator described here.
- How to update an ADR (without breaking the audit trail) — the supersession protocol that depends on numbers being stable forever.
- The Nygard ADR template — the body shape that goes inside the numbered file.
- ADR template in Markdown — drop-in template; pair with a 4-digit numbering scheme out of the box.
- ADR template for GitHub repos — folder layout and PR template that this numbering scheme assumes.
- MADR (Markdown ADR) template — MADR's filename convention is the same 4-digit pattern; this page's allocator works unchanged.
- When to write an ADR (and when you shouldn't) — the threshold question; this page picks up after the answer is yes.
- How to document architecture decisions — the practice-level frame for why directory operations matter.
- The open-source extractor — surfaces decisions from your AI chat exports so they reach the numbered directory in the first place.