{"course":{"slug":"how-software-changed","title":"How Software Development Changed","url":"https://addaly.com/learn/how-software-changed","level":"intermediate","track":"work"},"lesson":{"slug":"specification-got-more-valuable","title":"Why writing it down matters more now","url":"https://addaly.com/learn/how-software-changed/specification-got-more-valuable","position":30,"of":87,"minutes":8,"answer":"Ambiguity used to stop you while typing; now it gets resolved for you, silently, in some direction.","keyIdea":"Ambiguity used to stop you while typing; now it gets resolved for you, silently, in some direction."},"body":{"format":"markdown","text":"## The old argument for skipping the spec\n\nFor twenty years the case against heavy specification was good. Requirements change, documents rot, and (the strongest point) writing the code *was* the thinking. You discovered the ambiguities by hitting them. Halfway through the function you realised nobody had said what happens on a tie, so you went and asked. The typing was slow enough to be a thinking process.\n\nThat mechanism is gone, or at least much weaker. When a draft takes ninety seconds, the ambiguity does not stop you. It gets resolved silently, plausibly, in whatever direction the priors point, and you receive a confident answer to a question you never asked.\n\nSo the thinking has to move somewhere. It moves to before.\n\n> Figure: What happens to a question nobody asked. Steps: 1. A request arrives that sounds complete (Add rate limiting to the signup endpoint. Five decisions are unstated and every one of them changes the code.); 2. A draft appears in ninety seconds (The typing is no longer slow enough to stop you when you reach the ambiguity.); 3. The decisions get made anyway (Silently, in whichever direction the priors point. Fail open or fail closed is answered by what is most common rather than by your incident history.); 4. The change can only be checked against itself (Internally consistent, sensibly named, and answering a question nobody chose.). Ambiguity used to stop you while you typed, which is how it got noticed. That mechanism is gone, so the thinking has to move to before.\n\n## What a useful specification looks like now\n\nNot forty pages. Usually one, sometimes three paragraphs. It has to contain the things a competent stranger could not guess.\n\nTake a request that sounds complete: *add rate limiting to the signup endpoint.*\n\nEvery one of these is unspecified, and every one changes the code:\n\n- Per IP address, per account, per device, or some combination?\n- What window, and what limit? Ten per hour or three per minute are different systems.\n- What happens at the limit? A 429 with `Retry-After`, a silent delay, a CAPTCHA, a shadow ban?\n- What about shared addresses: a university NAT in Lagos, a corporate gateway in Jakarta, a mobile carrier in São Paulo putting a hundred thousand users behind one IPv4 address? Per-IP limiting will lock out a whole campus.\n- When the limiter's own storage is unreachable, does signup fail open or fail closed?\n\nThat last question has no technically correct answer. Fail open and an outage of the limiter becomes an open door for abuse. Fail closed and an outage of the limiter becomes an outage of signup. It is a business decision with a security consequence, it depends on what your company can survive, and nothing that has not sat in your incident reviews can make it for you.\n\nA spec that answers those five questions turns a vague request into work that can be done correctly by anyone, or anything, and checked afterwards.\n\n## Write the decision, not just the requirement\n\nThe sentence that survives longest is the one that says why:\n\n```markdown\n## Rate limit: POST /signup\n\nKey: (ip, account_email_domain). 5 attempts / 10 min, sliding window.\nOver limit: 429 + Retry-After. No CAPTCHA in v1.\n\nOn Redis unavailable: FAIL OPEN.\nWhy: signup outage costs more than a burst of abuse we can clean up\nafterwards. Revisit if abuse volume exceeds ~200 fake accounts/day.\n\nOut of scope: password reset, OAuth callback, mobile app endpoints.\n```\n\nSix months from now, someone will see the fail-open branch and assume it is a bug. Those two lines are the difference between them fixing a decision and them respecting it. This is what architecture decision records were always for; the practice just went from nice-to-have to load-bearing.\n\n## The out-of-scope line is doing real work\n\nGenerated changes sprawl. Ask for a rate limiter and you may get one on four endpoints, plus a config module, plus a metrics wrapper. Each addition is defensible in isolation, and together they are a change nobody scoped and nobody can review properly.\n\nStating what is out of scope is the cheapest control you have over blast radius, and it works on human collaborators too.\n\n## One artifact, three jobs\n\nThe interesting part: the spec is now also the prompt, and the acceptance criteria are now also the test names. The same page tells the model what to build, tells the reviewer what to check against, and tells the test suite what to assert. When they are one artifact, they cannot drift apart.\n\nWhich brings the one rule that makes this work at all: **the spec lives in the repository, next to the code, and changes in the same commit.** A specification that drifts is worse than none, because it is wrong with authority. If updating it is a separate ceremony in a separate tool, it will drift by the second week.\n\n## The caveat\n\nNone of this means writing a design document for a two-line fix. The rule of thumb: write the spec when a wrong guess would be expensive or invisible. Renaming a variable, no. Anything touching money, permissions, deletion, or another company's API, yes. Anything where the phrase \"it depends what we mean by\" comes up in conversation, yes, and write down which meaning you picked."},"check":{"question":"Your ticket says: \"Deduplicate the nightly customer import.\" A complete-looking implementation arrives in ten minutes and the tests pass. What is the most important thing the ticket failed to say?","options":[{"text":"Which language and library to use, so the implementation matches the rest of the codebase.","correct":false,"tempting":"House conventions are usually inferable from surrounding code, and getting them wrong is visible in review rather than silent in production."},{"text":"Which record wins when two rows describe the same customer, and what counts as the same customer in the first place.","correct":true},{"text":"The expected performance target, since deduplication over a large import can be quadratic if written carelessly.","correct":false,"tempting":"A genuine risk, but a slow job announces itself the first night it runs; a wrong merge rule quietly corrupts customer records for months."},{"text":"Whether the change needs tests, and what coverage level the team expects for import code.","correct":false,"tempting":"Asks about the process around the work rather than the ambiguity inside it, which is the thing that gets resolved for you without your knowledge."}]},"prev":{"slug":"release-as-verification","title":"Verification that continues after merge","url":"https://addaly.com/learn/how-software-changed/release-as-verification"},"next":{"slug":"the-ambiguity-audit","title":"The ambiguity audit","url":"https://addaly.com/learn/how-software-changed/the-ambiguity-audit"},"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) 'Why writing it down matters more now', in How Software Development Changed. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/how-software-changed/specification-got-more-valuable (Accessed: 31 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"Why writing it down matters more now","courseTitle":"How Software Development Changed","module":{"index":4,"slug":"saying-what-you-want","title":"Saying what you want"},"answer":null,"keyIdea":"Ambiguity used to stop you while typing; now it gets resolved for you, silently, in some direction.","hero":{"kind":"figure","figure":{"t":"figure","label":"Figure 4.1","figure":{"type":"flow","steps":[{"label":"A request arrives that sounds complete","detail":"Add rate limiting to the signup endpoint. Five decisions are unstated and every one of them changes the code."},{"label":"A draft appears in ninety seconds","detail":"The typing is no longer slow enough to stop you when you reach the ambiguity."},{"label":"The decisions get made anyway","detail":"Silently, in whichever direction the priors point. Fail open or fail closed is answered by what is most common rather than by your incident history."},{"label":"The change can only be checked against itself","detail":"Internally consistent, sensibly named, and answering a question nobody chose."}],"title":"What happens to a question nobody asked","caption":"Ambiguity used to stop you while you typed, which is how it got noticed. That mechanism is gone, so the thinking has to move to before."},"alt":"Figure: What happens to a question nobody asked. Steps: 1. A request arrives that sounds complete (Add rate limiting to the signup endpoint. Five decisions are unstated and every one of them changes the code.); 2. A draft appears in ninety seconds (The typing is no longer slow enough to stop you when you reach the ambiguity.); 3. The decisions get made anyway (Silently, in whichever direction the priors point. Fail open or fail closed is answered by what is most common rather than by your incident history.); 4. The change can only be checked against itself (Internally consistent, sensibly named, and answering a question nobody chose.). Ambiguity used to stop you while you typed, which is how it got noticed. That mechanism is gone, so the thinking has to move to before."}},"blocks":[{"t":"h2","id":"the-old-argument-for-skipping-the-spec","runs":[{"text":"The old argument for skipping the spec"}]},{"t":"p","runs":[{"text":"For twenty years the case against heavy specification was good. Requirements change, documents rot, and (the strongest point) writing the code "},{"text":"was","i":true},{"text":" the thinking. You discovered the ambiguities by hitting them. Halfway through the function you realised nobody had said what happens on a tie, so you went and asked. The typing was slow enough to be a thinking process."}]},{"t":"p","runs":[{"text":"That mechanism is gone, or at least much weaker. When a draft takes ninety seconds, the ambiguity does not stop you. It gets resolved silently, plausibly, in whatever direction the priors point, and you receive a confident answer to a question you never asked."}]},{"t":"p","runs":[{"text":"So the thinking has to move somewhere. It moves to before."}]},{"t":"h2","id":"what-a-useful-specification-looks-like-now","runs":[{"text":"What a useful specification looks like now"}]},{"t":"p","runs":[{"text":"Not forty pages. Usually one, sometimes three paragraphs. It has to contain the things a competent stranger could not guess."}]},{"t":"p","runs":[{"text":"Take a request that sounds complete: "},{"text":"add rate limiting to the signup endpoint.","i":true}]},{"t":"p","runs":[{"text":"Every one of these is unspecified, and every one changes the code:"}]},{"t":"ul","items":[[{"text":"Per IP address, per account, per device, or some combination?"}],[{"text":"What window, and what limit? Ten per hour or three per minute are different systems."}],[{"text":"What happens at the limit? A 429 with "},{"text":"Retry-After","code":true},{"text":", a silent delay, a CAPTCHA, a shadow ban?"}],[{"text":"What about shared addresses: a university NAT in Lagos, a corporate gateway in Jakarta, a mobile carrier in São Paulo putting a hundred thousand users behind one IPv4 address? Per-IP limiting will lock out a whole campus."}],[{"text":"When the limiter’s own storage is unreachable, does signup fail open or fail closed?"}]]},{"t":"p","runs":[{"text":"That last question has no technically correct answer. Fail open and an outage of the limiter becomes an open door for abuse. Fail closed and an outage of the limiter becomes an outage of signup. It is a business decision with a security consequence, it depends on what your company can survive, and nothing that has not sat in your incident reviews can make it for you."}]},{"t":"p","runs":[{"text":"A spec that answers those five questions turns a vague request into work that can be done correctly by anyone, or anything, and checked afterwards."}]},{"t":"h2","id":"write-the-decision-not-just-the-requirement","runs":[{"text":"Write the decision, not just the requirement"}]},{"t":"p","runs":[{"text":"The sentence that survives longest is the one that says why:"}]},{"t":"code","lang":"markdown","label":"Markdown","text":"## Rate limit: POST /signup\n\nKey: (ip, account_email_domain). 5 attempts / 10 min, sliding window.\nOver limit: 429 + Retry-After. No CAPTCHA in v1.\n\nOn Redis unavailable: FAIL OPEN.\nWhy: signup outage costs more than a burst of abuse we can clean up\nafterwards. Revisit if abuse volume exceeds ~200 fake accounts/day.\n\nOut of scope: password reset, OAuth callback, mobile app endpoints."},{"t":"p","runs":[{"text":"Six months from now, someone will see the fail-open branch and assume it is a bug. Those two lines are the difference between them fixing a decision and them respecting it. This is what architecture decision records were always for; the practice just went from nice-to-have to load-bearing."}]},{"t":"h2","id":"the-out-of-scope-line-is-doing-real-work","runs":[{"text":"The out-of-scope line is doing real work"}]},{"t":"p","runs":[{"text":"Generated changes sprawl. Ask for a rate limiter and you may get one on four endpoints, plus a config module, plus a metrics wrapper. Each addition is defensible in isolation, and together they are a change nobody scoped and nobody can review properly."}]},{"t":"p","runs":[{"text":"Stating what is out of scope is the cheapest control you have over blast radius, and it works on human collaborators too."}]},{"t":"h2","id":"one-artifact-three-jobs","runs":[{"text":"One artifact, three jobs"}]},{"t":"p","runs":[{"text":"The interesting part: the spec is now also the prompt, and the acceptance criteria are now also the test names. The same page tells the model what to build, tells the reviewer what to check against, and tells the test suite what to assert. When they are one artifact, they cannot drift apart."}]},{"t":"p","runs":[{"text":"Which brings the one rule that makes this work at all: "},{"text":"the spec lives in the repository, next to the code, and changes in the same commit.","b":true},{"text":" A specification that drifts is worse than none, because it is wrong with authority. If updating it is a separate ceremony in a separate tool, it will drift by the second week."}]},{"t":"h2","id":"the-caveat","runs":[{"text":"The caveat"}]},{"t":"p","runs":[{"text":"None of this means writing a design document for a two-line fix. The rule of thumb: write the spec when a wrong guess would be expensive or invisible. Renaming a variable, no. Anything touching money, permissions, deletion, or another company’s API, yes. Anything where the phrase “it depends what we mean by” comes up in conversation, yes, and write down which meaning you picked."}]}],"check":{"question":[{"text":"Your ticket says: “Deduplicate the nightly customer import.” A complete-looking implementation arrives in ten minutes and the tests pass. What is the most important thing the ticket failed to say?"}],"options":[{"text":[{"text":"Which language and library to use, so the implementation matches the rest of the codebase."}],"correct":false,"tempting":[{"text":"House conventions are usually inferable from surrounding code, and getting them wrong is visible in review rather than silent in production."}]},{"text":[{"text":"Which record wins when two rows describe the same customer, and what counts as the same customer in the first place."}],"correct":true,"tempting":null},{"text":[{"text":"The expected performance target, since deduplication over a large import can be quadratic if written carelessly."}],"correct":false,"tempting":[{"text":"A genuine risk, but a slow job announces itself the first night it runs; a wrong merge rule quietly corrupts customer records for months."}]},{"text":[{"text":"Whether the change needs tests, and what coverage level the team expects for import code."}],"correct":false,"tempting":[{"text":"Asks about the process around the work rather than the ambiguity inside it, which is the thing that gets resolved for you without your knowledge."}]}],"order":[2,1,3,0],"why":null},"prev":{"slug":"release-as-verification","title":"Verification that continues after merge","minutes":9},"next":{"slug":"the-ambiguity-audit","title":"The ambiguity audit","minutes":10}}}