references/decisions.md
Decisions: reversibility and the record
Most decisions should be made quickly and quietly. A few will still be shaping the system in five years. This file is about telling them apart and writing down the second kind.
Two kinds of door
Two-way doors can be walked back. Wrong choice of HTTP client, a component split in the wrong place, a slightly odd folder name. Cost of reversal: an afternoon. Make these fast, by whoever is closest to the work, with no meeting and no document. Deliberating over a two-way door is itself a cost, and a team that debates everything moves slowly and still gets the important ones wrong because the attention was spent.
One-way doors cannot. These deserve disproportionate care, a written record, and someone senior actually thinking.
The tell for a one-way door is specific and worth memorising:
Something outside your codebase has already copied the decision.
Persisted data has a shape. Published URLs are in bookmarks and integrations. A domain is baked into every login name. An identifier is in a customer's database. A public API signature is in code you cannot edit. Once the decision has left the building, editing your source does not take it back.
The second tell is a migration path that requires downtime or coordination with someone who does not work for you.
The one-way doors in this catalogue
Every skill here has one, and they are all the same shape. Worth reading as a set, because the pattern generalises to whatever you are working on next.
| Skill | Decision | Why it is permanent |
|---|---|---|
mongodb-production | Embed or reference | The shape is in production data, not in code |
mongodb-production | Multi-tenancy model | Real customer data exists in that arrangement |
zitadel-production | External domain and TLS mode | Written into the database at setup, and into every login name |
zitadel-production | Project as the trust boundary | Audience claims are already in issued tokens and in client validation code |
deploy-pipeline | The branch names | Environment names, image tags, env files, and Dokploy hooks all derive from them |
naming-conventions | Any name that reached a DB, a URL, or a client | Renaming is a migration with external coordination |
react-native-expo | The ui/ versus features/ line | Everything above the line was built to be portable, and coupling it is not detectable later |
apple-marketing-ui | Cloning the starter per project | The copies immediately begin to diverge |
Notice that none of them are technology choices. The irreversible decisions are almost never which framework. They are about shape, identity, and boundaries, which is exactly the category people spend the least time on because it does not feel like a decision at the time.
Before an irreversible decision
Four questions, ten minutes:
- What has to be true for this to be right? Name the assumption. Most bad
architecture is a correct decision under an assumption nobody wrote down and that stopped holding two years later.
- What would make me change my mind? If nothing would, you are not
reasoning, you are preferring.
- What is the reversal cost, concretely? Not "hard". A number of weeks and
a list of who else is involved.
- Can I make it two-way instead? Often yes, for less than you think. An
opaque cursor rather than an exposed offset. A UUID rather than a human-meaningful key. A version prefix on the API from day one. An adapter around the vendor. Buying reversibility is usually cheaper than buying certainty, and this is the single most useful move in the whole file.
The record
Keep it in the repo, next to the code, in docs/decisions/. Not a wiki, not a ticket, not a thread. Those are all places where the decision and the code drift apart, and where a new joiner or a model will never find it.
One file per decision, numbered, roughly a page:
# 0007. Tenant isolation via a shared collection and a required filter
Date: 2026-07-25
Status: accepted
## Context
Twelve client apps, one cluster. Database per tenant degrades past a few
thousand databases and we expect to cross that. One client (Acme) has a
contractual isolation requirement.
## Decision
Shared collections with a required `tenantId`, enforced by a data-access layer
that cannot construct a query without one. Acme routed to a dedicated database
through the same interface.
## Consequences
Every index gains `tenantId` as its first field. Cross-tenant analytics needs a
separate read path. A missing filter becomes structurally impossible rather than
a review item. Adding a second isolated tenant later is a routing change, not a
migration.
## Assumption
That per-tenant SSO is not needed by more than one client. If three clients ask
for it, revisit.
Five headings, and the last two carry all the value.
Consequences is what makes the record worth writing. Anyone can find the decision by reading the code. What they cannot recover is what you knowingly gave up, which means the next person re-litigates a trade-off you already resolved, or worse, quietly reverses it without knowing there was one.
Assumption is what makes it worth keeping. It converts the record from history into a tripwire. "If three clients ask for per-tenant SSO, revisit" is an instruction to the future, and it is the only part of the document that can save someone from carrying a dead constraint for years.
Status, and superseding
proposed, accepted, superseded by 0019. Never delete a decision record and never edit its substance after acceptance. Write a new one and mark the old one superseded.
The old record is not clutter. It is the answer to "why was it ever like that", which is the question that precedes every dangerous change, and the absence of an answer is what makes people either freeze or break something.
When to write one
Not for everything. The bar:
- The decision is a one-way door by the tests above
- Or it was contested, and the argument will otherwise be had again
- Or it looks wrong from the outside, and someone will "fix" it
That last one is the underrated case. Code that looks like a mistake and is not attracts well-meaning corrections forever. A record, or sometimes just a comment naming the constraint, is the cheapest possible defence.
Do not write one for a choice with an obvious default and no argument. A decision log that contains everything gets read by nobody, which is the same as not having one, but with more work.
Comments as micro-records
Most "why" does not deserve a file. It deserves a line, at the point of confusion.
# Retry twice: the provider's 502 is transient and their SLA covers a second
# attempt. Three retries breaches our own p99 budget. See docs/decisions/0012.
The rule for comments is the same as for records: the code says what, the comment says why. A comment restating the line below it is volume, and it goes stale the moment the line changes. A comment naming a constraint that is not visible locally is the only documentation that cannot be derived from reading harder.
The highest-value comments, by a distance, are the ones recording a non-obvious external constraint: an upstream bug being worked around, an ordering that matters, a limit imposed by someone else's system, a value that looks arbitrary and is not. Those are the ones that stop a future change from being confidently wrong.