{"course":{"slug":"how-software-changed","title":"How Software Development Changed","url":"https://addaly.com/learn/how-software-changed","level":"intermediate","track":"work"},"lesson":{"slug":"scope-and-blast-radius","title":"Bounding what a change is allowed to touch","url":"https://addaly.com/learn/how-software-changed/scope-and-blast-radius","position":36,"of":87,"minutes":9,"answer":"A change with no stated boundary expands to the complete version of what the tool recognises, so name what is out of scope, forbid new dependencies by default.","keyIdea":"A change with no stated boundary expands to the complete version of what the tool recognises, so name what is out of scope, forbid new dependencies by default, and instruct it to stop and ask rather than route around an obstacle."},"body":{"format":"markdown","text":"## Sprawl is the default, and it is nobody's fault\n\nAsk for a rate limiter and you may get: a rate limiter, applied to four endpoints instead of one, plus a configuration module, plus a metrics wrapper, plus a refactor of the middleware chain, plus a new dependency, plus updated tests for all of it.\n\nEvery addition is defensible on its own. Together they are a change nobody scoped, nobody sized, and nobody can review properly, and the two-thirds of it that was not requested is the part that will break something.\n\nThe mechanism is worth naming rather than complaining about. A system asked to produce something good, with no boundary, will produce the complete version of what it recognises. Completeness is a virtue in the abstract and a liability inside a running system, where the value of a change is inversely proportional to how much of the system it disturbs.\n\n## State the boundary explicitly, in the specification\n\nThe out-of-scope line is the cheapest control you have and it takes one sentence.\n\n```markdown\n## In scope\nRate limiting on POST /signup only.\n\n## Out of scope\n- Password reset, OAuth callback, the mobile endpoints\n- Any change to the existing middleware chain\n- Metrics beyond the counter we already emit\n- New dependencies\n\n## If you think the schema needs to change, stop and say so.\n```\n\nThat last line is the important one and it deserves its own treatment.\n\n## \"Say so and stop\"\n\nThe default behaviour when a system meets an obstacle is to route around it. It will invent the missing column, assume the missing configuration value, work around the constraint, or make the failing test pass by changing the test.\n\nNone of those is a decision it is qualified to make, and all of them arrive dressed as decisions rather than as questions.\n\nThe instruction that changes this is short and works remarkably well:\n\n> If a requirement is unclear, if the schema needs to change, if this needs a new dependency, or if a test fails for a reason you do not understand: stop and say so. Do not guess.\n\nAdd it to your conventions file and to any non-trivial request. The output changes shape: instead of a confident diff built on a wrong assumption, you get a question, which costs thirty seconds instead of an afternoon.\n\nThe same instruction is worth giving human collaborators, and for the same reason. \"Ask rather than assume\" is the same policy; the difference is that a person will usually ask anyway when the stakes are visible, and a model has no sense of stakes.\n\n## Bounding tactics that work\n\n**One verb per change.** *Add* a column, or *use* a column, not both. *Move* code, or *change* it, not both. If your description needs the word \"and\", consider two changes.\n\n**Name the files that may be touched.** \"Change only `billing/fx.py` and its test.\" Then check the diff against the list, which takes four seconds and catches everything.\n\n**Forbid new dependencies by default.** Adding one is a supply-chain decision, and it should be a deliberate one with its own conversation, not a side effect of solving a formatting problem.\n\n**Set a size expectation up front.** \"This should be about 30 lines.\" If what comes back is 300, that gap is information: either the problem was harder than you thought, or the response is doing things you did not ask for. Either way you want to know before you start reading.\n\n**Check the file list before the code.** Open the diff, read the list of changed paths, and compare it with what you asked for. Anything unexpected there gets explained before you spend attention on the lines.\n\n## The refactor trap\n\nOne case deserves calling out because it is so common and so reasonable-sounding.\n\nYou ask for a small behaviour change in a messy function. What comes back is a clean, well-structured rewrite of the whole function, with the change in it. The rewrite is better code.\n\nAccepting it is still a mistake, because you have converted a five-line reviewable change into a hundred-line one, and the behaviour change is now hidden inside a diff where every line is different. Nobody, including you, can tell whether the rewrite preserved the six edge cases that made the function ugly in the first place. Ugly functions are usually ugly for reasons.\n\nThe correct move is the split from earlier in this course: take the rewrite as its own change, verified as behaviour-preserving against the existing tests, and then make the five-line change on top. Two reviews, each of which means something, instead of one that does not."},"check":{"question":"A request for a five-line behaviour change returns a clean rewrite of the whole function with the change included. The rewrite is genuinely better structured. Why should it not be merged as one change?","options":[{"text":"Rewrites of working code are rarely worth the risk, so the correct response is to reject the restructuring and keep the original shape.","correct":false,"tempting":"Discards a real improvement; the restructuring is fine, the problem is bundling it with a behaviour change."},{"text":"The rewrite will conflict with other branches touching the same function, creating merge work for the rest of the team.","correct":false,"tempting":"A genuine cost of large diffs, but it would apply equally to a well-separated refactor commit, which is the right answer here."},{"text":"The behaviour change is now invisible inside a diff where every line differs.","correct":true},{"text":"Generated rewrites tend to drop edge cases, so the original function's complexity should be treated as load-bearing until proven otherwise.","correct":false,"tempting":"Right that ugly functions are often ugly for reasons, but this treats the risk as unverifiable when the existing tests can settle it."}]},"prev":{"slug":"interfaces-before-implementations","title":"Design the seam, generate the filling","url":"https://addaly.com/learn/how-software-changed/interfaces-before-implementations"},"next":{"slug":"examples-beat-adjectives","title":"Examples beat adjectives","url":"https://addaly.com/learn/how-software-changed/examples-beat-adjectives"},"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) 'Bounding what a change is allowed to touch', in How Software Development Changed. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/how-software-changed/scope-and-blast-radius (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":"Bounding what a change is allowed to touch","courseTitle":"How Software Development Changed","module":{"index":4,"slug":"saying-what-you-want","title":"Saying what you want"},"answer":null,"keyIdea":"A change with no stated boundary expands to the complete version of what the tool recognises, so name what is out of scope, forbid new dependencies by default, and instruct it to stop and ask rather than route around an obstacle.","hero":{"kind":"plate","image":null,"alt":null,"caption":null},"blocks":[{"t":"h2","id":"sprawl-is-the-default-and-it-is-nobody-s-fault","runs":[{"text":"Sprawl is the default, and it is nobody’s fault"}]},{"t":"p","runs":[{"text":"Ask for a rate limiter and you may get: a rate limiter, applied to four endpoints instead of one, plus a configuration module, plus a metrics wrapper, plus a refactor of the middleware chain, plus a new dependency, plus updated tests for all of it."}]},{"t":"p","runs":[{"text":"Every addition is defensible on its own. Together they are a change nobody scoped, nobody sized, and nobody can review properly, and the two-thirds of it that was not requested is the part that will break something."}]},{"t":"p","runs":[{"text":"The mechanism is worth naming rather than complaining about. A system asked to produce something good, with no boundary, will produce the complete version of what it recognises. Completeness is a virtue in the abstract and a liability inside a running system, where the value of a change is inversely proportional to how much of the system it disturbs."}]},{"t":"h2","id":"state-the-boundary-explicitly-in-the-specification","runs":[{"text":"State the boundary explicitly, in the specification"}]},{"t":"p","runs":[{"text":"The out-of-scope line is the cheapest control you have and it takes one sentence."}]},{"t":"code","lang":"markdown","label":"Markdown","text":"## In scope\nRate limiting on POST /signup only.\n\n## Out of scope\n- Password reset, OAuth callback, the mobile endpoints\n- Any change to the existing middleware chain\n- Metrics beyond the counter we already emit\n- New dependencies\n\n## If you think the schema needs to change, stop and say so."},{"t":"p","runs":[{"text":"That last line is the important one and it deserves its own treatment."}]},{"t":"h2","id":"say-so-and-stop","runs":[{"text":"“Say so and stop”"}]},{"t":"p","runs":[{"text":"The default behaviour when a system meets an obstacle is to route around it. It will invent the missing column, assume the missing configuration value, work around the constraint, or make the failing test pass by changing the test."}]},{"t":"p","runs":[{"text":"None of those is a decision it is qualified to make, and all of them arrive dressed as decisions rather than as questions."}]},{"t":"p","runs":[{"text":"The instruction that changes this is short and works remarkably well:"}]},{"t":"quote","runs":[{"text":"If a requirement is unclear, if the schema needs to change, if this needs a new dependency, or if a test fails for a reason you do not understand: stop and say so. Do not guess."}]},{"t":"p","runs":[{"text":"Add it to your conventions file and to any non-trivial request. The output changes shape: instead of a confident diff built on a wrong assumption, you get a question, which costs thirty seconds instead of an afternoon."}]},{"t":"p","runs":[{"text":"The same instruction is worth giving human collaborators, and for the same reason. “Ask rather than assume” is the same policy; the difference is that a person will usually ask anyway when the stakes are visible, and a model has no sense of stakes."}]},{"t":"h2","id":"bounding-tactics-that-work","runs":[{"text":"Bounding tactics that work"}]},{"t":"p","runs":[{"text":"One verb per change.","b":true},{"text":" "},{"text":"Add","i":true},{"text":" a column, or "},{"text":"use","i":true},{"text":" a column, not both. "},{"text":"Move","i":true},{"text":" code, or "},{"text":"change","i":true},{"text":" it, not both. If your description needs the word “and”, consider two changes."}]},{"t":"p","runs":[{"text":"Name the files that may be touched.","b":true},{"text":" “Change only "},{"text":"billing/fx.py","code":true},{"text":" and its test.” Then check the diff against the list, which takes four seconds and catches everything."}]},{"t":"p","runs":[{"text":"Forbid new dependencies by default.","b":true},{"text":" Adding one is a supply-chain decision, and it should be a deliberate one with its own conversation, not a side effect of solving a formatting problem."}]},{"t":"p","runs":[{"text":"Set a size expectation up front.","b":true},{"text":" “This should be about 30 lines.” If what comes back is 300, that gap is information: either the problem was harder than you thought, or the response is doing things you did not ask for. Either way you want to know before you start reading."}]},{"t":"p","runs":[{"text":"Check the file list before the code.","b":true},{"text":" Open the diff, read the list of changed paths, and compare it with what you asked for. Anything unexpected there gets explained before you spend attention on the lines."}]},{"t":"h2","id":"the-refactor-trap","runs":[{"text":"The refactor trap"}]},{"t":"p","runs":[{"text":"One case deserves calling out because it is so common and so reasonable-sounding."}]},{"t":"p","runs":[{"text":"You ask for a small behaviour change in a messy function. What comes back is a clean, well-structured rewrite of the whole function, with the change in it. The rewrite is better code."}]},{"t":"p","runs":[{"text":"Accepting it is still a mistake, because you have converted a five-line reviewable change into a hundred-line one, and the behaviour change is now hidden inside a diff where every line is different. Nobody, including you, can tell whether the rewrite preserved the six edge cases that made the function ugly in the first place. Ugly functions are usually ugly for reasons."}]},{"t":"p","runs":[{"text":"The correct move is the split from earlier in this course: take the rewrite as its own change, verified as behaviour-preserving against the existing tests, and then make the five-line change on top. Two reviews, each of which means something, instead of one that does not."}]}],"check":{"question":[{"text":"A request for a five-line behaviour change returns a clean rewrite of the whole function with the change included. The rewrite is genuinely better structured. Why should it not be merged as one change?"}],"options":[{"text":[{"text":"Rewrites of working code are rarely worth the risk, so the correct response is to reject the restructuring and keep the original shape."}],"correct":false,"tempting":[{"text":"Discards a real improvement; the restructuring is fine, the problem is bundling it with a behaviour change."}]},{"text":[{"text":"The rewrite will conflict with other branches touching the same function, creating merge work for the rest of the team."}],"correct":false,"tempting":[{"text":"A genuine cost of large diffs, but it would apply equally to a well-separated refactor commit, which is the right answer here."}]},{"text":[{"text":"The behaviour change is now invisible inside a diff where every line differs."}],"correct":true,"tempting":null},{"text":[{"text":"Generated rewrites tend to drop edge cases, so the original function’s complexity should be treated as load-bearing until proven otherwise."}],"correct":false,"tempting":[{"text":"Right that ugly functions are often ugly for reasons, but this treats the risk as unverifiable when the existing tests can settle it."}]}],"order":[1,3,0,2],"why":null},"prev":{"slug":"interfaces-before-implementations","title":"Design the seam, generate the filling","minutes":9},"next":{"slug":"examples-beat-adjectives","title":"Examples beat adjectives","minutes":8}}}