{"course":{"slug":"how-software-changed","title":"How Software Development Changed","url":"https://addaly.com/learn/how-software-changed","level":"intermediate","track":"work"},"lesson":{"slug":"decision-records","title":"Writing down the why","url":"https://addaly.com/learn/how-software-changed/decision-records","position":33,"of":87,"minutes":9,"answer":"A decision record earns its place through the alternatives it rejects and the costs it admits.","keyIdea":"A decision record earns its place through the alternatives it rejects and the costs it admits, and a one-line comment linking code to its record is what stops a deliberate oddity from being helpfully corrected later."},"body":{"format":"markdown","text":"## Code says what, never why\n\nRead any line of code and you can work out what it does. You cannot work out what else was considered, what was tried and abandoned, or which constraint made the obvious approach impossible. That information exists only in someone's memory, and memory leaves the company.\n\nThis was always a problem. It became a sharper one, because the memory used to belong to a person who had spent a day on the decision, and it now sometimes belongs to a chat session that was closed. A decision record is the only durable place for it.\n\n## The format, which should stay small\n\nAn architecture decision record is one file, in the repository, numbered, immutable once accepted. The original form from Michael Nygard is five headings and nobody has improved on it:\n\n```markdown\n# 0014 — Store converted amounts on the invoice line\n\nStatus: accepted (2026-08-14)\n\n## Context\nFinance needs invoices in the currency charged, with the total in\nthe account's base currency. Rates move daily. Invoices are legal\ndocuments in three of our markets and must not change after issue.\n\n## Decision\nStore both the original amount and the converted amount on each\nline, plus the rate and the timestamp used. Compute at write time.\n\n## Consequences\n+ Invoices are immutable; a rate change cannot alter a past document.\n+ Reprinting an old invoice needs no rate lookup.\n− Three new columns and a backfill over 4.1M rows.\n− If we discover a rate was wrong, correcting it means a credit note,\n  not an update. Finance has confirmed this is what they want.\n\n## Alternatives considered\n- Compute at read time from a rate table. Rejected: past invoices\n  would silently change when rates were corrected.\n- Store only the base-currency total. Rejected: customers in Jakarta\n  need to see what they were actually charged.\n```\n\nUnder a page. Fifteen minutes to write. Readable in ninety seconds in two years.\n\n## The section that matters most is the one people skip\n\n**Alternatives considered.** A record without it is a description of what happened, which the code already provides.\n\nThe purpose of an ADR is to stop a future engineer, or a future you, from spending three days rediscovering why the obvious approach does not work. The alternatives section is where that saving lives. Two lines per rejected option: what it was, and the specific reason it failed.\n\nThe second-most-skipped section is the negative half of *Consequences*. A record that lists only benefits is advocacy. Writing the costs down is what makes it possible to revisit the decision honestly when the costs turn out to be larger than expected.\n\n## What earns a record\n\nNot everything. A rough test: **would somebody reasonably assume this is a mistake?**\n\n- Fail open rather than fail closed on a rate limiter\n- Storing money as an integer of minor units rather than a decimal type\n- Not using the framework's built-in mechanism for something it does provide\n- Choosing one database, queue or provider over another\n- Deliberately duplicating code rather than extracting a shared abstraction\n- Any decision where the team argued for more than twenty minutes\n\nThat last one is the most reliable trigger. If it took an argument, it needs a record, because the argument will recur and the second one will be less informed than the first.\n\n## Where they live and how they die\n\nIn the repository, in `docs/adr/`, in the same pull request as the code they describe. Not in a wiki, not in a ticketing system, not in a shared drive. The reason is simple and empirical: a document in a separate tool is not read by anyone who is reading the code, and it drifts within weeks.\n\nRecords are immutable. You do not edit 0014 when the decision changes. You write 0021, mark it as superseding 0014, and add one line to 0014 pointing forward. The history of a decision is often more informative than its current state (it tells you what changed and what you learned) and editing in place destroys exactly that.\n\n## The specific reason this got more valuable\n\nGenerated code is locally sensible and globally uninformed. It will helpfully \"fix\" a deliberate oddity, because the oddity looks like an error and the reason for it is not in the file.\n\nThe fail-open branch on your rate limiter looks like a missing else. The duplicated function looks like something that should be extracted. The manual retry loop looks like it should use the library's decorator, which will retry your non-idempotent POST.\n\nA one-line comment pointing at the record is the cheapest defence:\n\n```python\n# Fails open by design — see docs/adr/0009-rate-limiter-availability.md\nexcept RedisUnavailable:\n    return allow()\n```\n\nThat comment costs eight words and it is the difference between a future change respecting a decision and a future change silently reversing it. It works on human collaborators too, and it always did: what changed is how many collaborators there are and how quickly they arrive."},"check":{"question":"A team writes decision records containing Context, Decision and Consequences, but omits Alternatives Considered to keep them short. What specifically is lost?","options":[{"text":"The record no longer explains what the system does, so newcomers cannot use it to understand the architecture.","correct":false,"tempting":"Confuses an ADR with documentation; the code and the Decision section already say what the system does."},{"text":"The protection against a future engineer re-attempting an approach that was already ruled out.","correct":true},{"text":"Traceability, since without alternatives there is no way to audit whether the decision followed a proper process.","correct":false,"tempting":"Frames the record as governance paperwork, which is how ADRs become a ritual nobody reads or writes honestly."},{"text":"The ability to supersede the record later, because a new decision cannot be justified without knowing what the old options were.","correct":false,"tempting":"Superseding needs the context and the consequences, both of which are present; the alternatives make it cheaper rather than possible."}]},"prev":{"slug":"acceptance-criteria-as-tests","title":"Criteria that become test names","url":"https://addaly.com/learn/how-software-changed/acceptance-criteria-as-tests"},"next":{"slug":"what-the-tool-can-know","title":"What the tool can actually know about your codebase","url":"https://addaly.com/learn/how-software-changed/what-the-tool-can-know"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'Writing down the why', in How Software Development Changed. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/how-software-changed/decision-records (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"Writing down the why","courseTitle":"How Software Development Changed","module":{"index":4,"slug":"saying-what-you-want","title":"Saying what you want"},"answer":null,"keyIdea":"A decision record earns its place through the alternatives it rejects and the costs it admits, and a one-line comment linking code to its record is what stops a deliberate oddity from being helpfully corrected later.","hero":{"kind":"plate","image":null,"alt":null,"caption":null},"blocks":[{"t":"h2","id":"code-says-what-never-why","runs":[{"text":"Code says what, never why"}]},{"t":"p","runs":[{"text":"Read any line of code and you can work out what it does. You cannot work out what else was considered, what was tried and abandoned, or which constraint made the obvious approach impossible. That information exists only in someone’s memory, and memory leaves the company."}]},{"t":"p","runs":[{"text":"This was always a problem. It became a sharper one, because the memory used to belong to a person who had spent a day on the decision, and it now sometimes belongs to a chat session that was closed. A decision record is the only durable place for it."}]},{"t":"h2","id":"the-format-which-should-stay-small","runs":[{"text":"The format, which should stay small"}]},{"t":"p","runs":[{"text":"An architecture decision record is one file, in the repository, numbered, immutable once accepted. The original form from Michael Nygard is five headings and nobody has improved on it:"}]},{"t":"code","lang":"markdown","label":"Markdown","text":"# 0014 — Store converted amounts on the invoice line\n\nStatus: accepted (2026-08-14)\n\n## Context\nFinance needs invoices in the currency charged, with the total in\nthe account's base currency. Rates move daily. Invoices are legal\ndocuments in three of our markets and must not change after issue.\n\n## Decision\nStore both the original amount and the converted amount on each\nline, plus the rate and the timestamp used. Compute at write time.\n\n## Consequences\n+ Invoices are immutable; a rate change cannot alter a past document.\n+ Reprinting an old invoice needs no rate lookup.\n− Three new columns and a backfill over 4.1M rows.\n− If we discover a rate was wrong, correcting it means a credit note,\n  not an update. Finance has confirmed this is what they want.\n\n## Alternatives considered\n- Compute at read time from a rate table. Rejected: past invoices\n  would silently change when rates were corrected.\n- Store only the base-currency total. Rejected: customers in Jakarta\n  need to see what they were actually charged."},{"t":"p","runs":[{"text":"Under a page. Fifteen minutes to write. Readable in ninety seconds in two years."}]},{"t":"h2","id":"the-section-that-matters-most-is-the-one-people-skip","runs":[{"text":"The section that matters most is the one people skip"}]},{"t":"p","runs":[{"text":"Alternatives considered.","b":true},{"text":" A record without it is a description of what happened, which the code already provides."}]},{"t":"p","runs":[{"text":"The purpose of an ADR is to stop a future engineer, or a future you, from spending three days rediscovering why the obvious approach does not work. The alternatives section is where that saving lives. Two lines per rejected option: what it was, and the specific reason it failed."}]},{"t":"p","runs":[{"text":"The second-most-skipped section is the negative half of "},{"text":"Consequences","i":true},{"text":". A record that lists only benefits is advocacy. Writing the costs down is what makes it possible to revisit the decision honestly when the costs turn out to be larger than expected."}]},{"t":"h2","id":"what-earns-a-record","runs":[{"text":"What earns a record"}]},{"t":"p","runs":[{"text":"Not everything. A rough test: "},{"text":"would somebody reasonably assume this is a mistake?","b":true}]},{"t":"ul","items":[[{"text":"Fail open rather than fail closed on a rate limiter"}],[{"text":"Storing money as an integer of minor units rather than a decimal type"}],[{"text":"Not using the framework’s built-in mechanism for something it does provide"}],[{"text":"Choosing one database, queue or provider over another"}],[{"text":"Deliberately duplicating code rather than extracting a shared abstraction"}],[{"text":"Any decision where the team argued for more than twenty minutes"}]]},{"t":"p","runs":[{"text":"That last one is the most reliable trigger. If it took an argument, it needs a record, because the argument will recur and the second one will be less informed than the first."}]},{"t":"h2","id":"where-they-live-and-how-they-die","runs":[{"text":"Where they live and how they die"}]},{"t":"p","runs":[{"text":"In the repository, in "},{"text":"docs/adr/","code":true},{"text":", in the same pull request as the code they describe. Not in a wiki, not in a ticketing system, not in a shared drive. The reason is simple and empirical: a document in a separate tool is not read by anyone who is reading the code, and it drifts within weeks."}]},{"t":"p","runs":[{"text":"Records are immutable. You do not edit 0014 when the decision changes. You write 0021, mark it as superseding 0014, and add one line to 0014 pointing forward. The history of a decision is often more informative than its current state (it tells you what changed and what you learned) and editing in place destroys exactly that."}]},{"t":"h2","id":"the-specific-reason-this-got-more-valuable","runs":[{"text":"The specific reason this got more valuable"}]},{"t":"p","runs":[{"text":"Generated code is locally sensible and globally uninformed. It will helpfully “fix” a deliberate oddity, because the oddity looks like an error and the reason for it is not in the file."}]},{"t":"p","runs":[{"text":"The fail-open branch on your rate limiter looks like a missing else. The duplicated function looks like something that should be extracted. The manual retry loop looks like it should use the library’s decorator, which will retry your non-idempotent POST."}]},{"t":"p","runs":[{"text":"A one-line comment pointing at the record is the cheapest defence:"}]},{"t":"code","lang":"python","label":"Python","text":"# Fails open by design — see docs/adr/0009-rate-limiter-availability.md\nexcept RedisUnavailable:\n    return allow()"},{"t":"p","runs":[{"text":"That comment costs eight words and it is the difference between a future change respecting a decision and a future change silently reversing it. It works on human collaborators too, and it always did: what changed is how many collaborators there are and how quickly they arrive."}]}],"check":{"question":[{"text":"A team writes decision records containing Context, Decision and Consequences, but omits Alternatives Considered to keep them short. What specifically is lost?"}],"options":[{"text":[{"text":"The record no longer explains what the system does, so newcomers cannot use it to understand the architecture."}],"correct":false,"tempting":[{"text":"Confuses an ADR with documentation; the code and the Decision section already say what the system does."}]},{"text":[{"text":"The protection against a future engineer re-attempting an approach that was already ruled out."}],"correct":true,"tempting":null},{"text":[{"text":"Traceability, since without alternatives there is no way to audit whether the decision followed a proper process."}],"correct":false,"tempting":[{"text":"Frames the record as governance paperwork, which is how ADRs become a ritual nobody reads or writes honestly."}]},{"text":[{"text":"The ability to supersede the record later, because a new decision cannot be justified without knowing what the old options were."}],"correct":false,"tempting":[{"text":"Superseding needs the context and the consequences, both of which are present; the alternatives make it cheaper rather than possible."}]}],"order":[3,0,1,2],"why":null},"prev":{"slug":"acceptance-criteria-as-tests","title":"Criteria that become test names","minutes":9},"next":{"slug":"what-the-tool-can-know","title":"What the tool can actually know about your codebase","minutes":9}}}