{"course":{"slug":"how-software-changed","title":"How Software Development Changed","url":"https://addaly.com/learn/how-software-changed","level":"intermediate","track":"work"},"lesson":{"slug":"diff-size-economics","title":"Why the diff has to be small","url":"https://addaly.com/learn/how-software-changed/diff-size-economics","position":14,"of":87,"minutes":9,"answer":"Defect detection per line falls as a review grows, because past a certain size a reviewer silently switches from comparing before and after states to pattern-matching on how the code looks.","keyIdea":"Defect detection per line falls as a review grows, because past a certain size a reviewer silently switches from comparing before and after states to pattern-matching on how the code looks, which is exactly the check generated code passes trivially."},"body":{"format":"markdown","text":"## The curve everybody in review has felt\n\nThe practitioner literature on code inspection (most of it originating in studies of large industrial teams and popularised through inspection tooling vendors) reports two consistent findings, and they have been quoted so often that it is worth stating them with proper hedging. Defect detection per line falls sharply once a single review passes somewhere in the range of a few hundred lines, and it falls sharply again once a review session runs beyond about an hour. The specific thresholds vary by study, by language and by domain, and the numbers you see quoted are averages over settings that may not resemble yours.\n\nWhat is not in doubt is the shape. Detection per line goes down as review size goes up, monotonically, in every measurement anyone has made. There is no plateau where a reviewer becomes efficient at scale.\n\n> Figure: Defects found per line, as a review grows. Line chart of defects found per hundred lines against lines in one review: The shape every inspection measurement has found (100, 9) (200, 7) (400, 4) (800, 2) (2000, 0.6). The thresholds vary by study, language and team, so these numbers are the shape rather than yours. What is not in doubt is that detection per line falls monotonically, with no size at which a reviewer becomes efficient.\n\nThat curve was always true. What changed is that producing a 2,000-line change now takes minutes, so the norm that used to be enforced by the cost of typing has to be enforced deliberately.\n\n## What a large diff does to a reviewer, mechanically\n\nIt is not laziness. It is capacity.\n\nUnderstanding a change means holding a model of the before-state and the after-state in mind simultaneously and comparing them. That model has to include the invariants: what was guaranteed before, what is guaranteed now. Every additional file adds to the model, and the models interact, so the cost is superlinear rather than linear.\n\nAt some point the model no longer fits, and what a reviewer does then is switch strategy without noticing. They stop comparing states and start pattern-matching on the lines: does this look like reasonable code? That is the exact check that generated code passes trivially. So a large diff does not merely reduce the amount of review. It converts review into the one form of review that no longer works.\n\n## How to actually split\n\n\"Make smaller pull requests\" is advice everyone has heard and few follow, because the mechanics are the obstacle. Three that work:\n\n**Separate the refactor from the change.** This is the highest-value split and the easiest to sell. First a commit that moves and renames things with zero behaviour change, verified by the existing tests. Then a commit that changes behaviour, which is now twenty lines instead of four hundred. The reviewer reads the second one properly because it is readable.\n\n**Stack the branches.** Branch B off branch A, open both, and note the dependency. Most git hosts show only the incremental diff for the second one. GitHub, GitLab and the `git-town` and `graphite` style tools all support this; so does plain git if you rebase carefully.\n\n```bash\ngit switch -c 1-add-currency-column main\n# ... commit ...\ngit switch -c 2-use-currency-in-total 1-add-currency-column\n```\n\n**Land it dark.** Merge the new code behind a flag that is off, in one reviewable piece, then flip it in a two-line change. The reviewer of the big piece knows nothing is live; the reviewer of the two-line piece knows exactly what changes.\n\n## Set a number and enforce it in CI\n\nA soft norm dissolves under deadline. A number in a pipeline does not.\n\n```yaml\n# fail the build on very large diffs, with an explicit override label\n- name: diff size\n  run: |\n    n=$(git diff --numstat origin/main...HEAD \\\n        | grep -vE '(lock|\\.snap|generated/)' \\\n        | awk '{s+=$1+$2} END {print s+0}')\n    echo \"changed lines: $n\"\n    test \"$n\" -lt 500 || test \"${OVERRIDE:-}\" = \"true\"\n```\n\nExclude lockfiles, snapshots and generated directories, because they are noise and blocking on them teaches people to ignore the rule. Provide a documented override, because there are real exceptions (a dependency bump across a hundred files, a formatter run, a genuine bulk rename) and a rule with no escape hatch gets disabled entirely within a month.\n\n## The counter-argument, which is real\n\nSplitting has a cost. Five stacked pull requests mean five review cycles, five CI runs, five context switches, and a merge order somebody has to remember. On a team where review latency is already three days, five sequential reviews is more than two weeks for one feature, and that is a worse outcome than one large review.\n\nSo the honest rule is conditional. Small diffs pay off when review turnaround is fast. If review takes days, splitting makes delivery worse, and the thing to fix first is the latency, which takes you back to the queueing lesson. The two problems are linked and solving them in the wrong order makes both worse."},"check":{"question":"A team introduces a 400-line cap on pull requests. Review latency is currently four days. Six weeks later, feature delivery has slowed. What is the most likely explanation?","options":[{"text":"The cap forced people to split changes badly, producing pull requests that cannot be understood in isolation.","correct":false,"tempting":"A real failure mode of careless splitting, but it would show up as confused reviews rather than as slower delivery overall."},{"text":"Reviewers are spending the same total time on more, smaller reviews, so the fixed overhead per review now dominates.","correct":false,"tempting":"Per-review overhead is real, but it is small compared with a four-day wait repeated for each piece of a split feature."},{"text":"Small diffs only reduce defects, never lead time, so any delivery slowdown must come from an unrelated change in the same period.","correct":false,"tempting":"Treats the two effects as independent when splitting directly multiplies the number of times a change passes through the queue."},{"text":"Each split feature now waits in the four-day queue several times over.","correct":true}]},"prev":{"slug":"the-review-budget","title":"Spending a review budget","url":"https://addaly.com/learn/how-software-changed/the-review-budget"},"next":{"slug":"reading-the-tests","title":"Reading a test suite you did not write","url":"https://addaly.com/learn/how-software-changed/reading-the-tests"},"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 the diff has to be small', in How Software Development Changed. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/how-software-changed/diff-size-economics (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":"Why the diff has to be small","courseTitle":"How Software Development Changed","module":{"index":2,"slug":"reading-what-you-did-not-write","title":"Reading what you did not write"},"answer":null,"keyIdea":"Defect detection per line falls as a review grows, because past a certain size a reviewer silently switches from comparing before and after states to pattern-matching on how the code looks, which is exactly the check generated code passes trivially.","hero":{"kind":"figure","figure":{"t":"figure","label":"Figure","figure":{"type":"line","title":"Defects found per line, as a review grows","series":[{"label":"The shape every inspection measurement has found","points":[[100,9],[200,7],[400,4],[800,2],[2000,0.6]]}],"xLabel":"lines in one review","yLabel":"defects found per hundred lines","caption":"The thresholds vary by study, language and team, so these numbers are the shape rather than yours. What is not in doubt is that detection per line falls monotonically, with no size at which a reviewer becomes efficient."},"alt":"Figure: Defects found per line, as a review grows. Line chart of defects found per hundred lines against lines in one review: The shape every inspection measurement has found (100, 9) (200, 7) (400, 4) (800, 2) (2000, 0.6). The thresholds vary by study, language and team, so these numbers are the shape rather than yours. What is not in doubt is that detection per line falls monotonically, with no size at which a reviewer becomes efficient."}},"blocks":[{"t":"h2","id":"the-curve-everybody-in-review-has-felt","runs":[{"text":"The curve everybody in review has felt"}]},{"t":"p","runs":[{"text":"The practitioner literature on code inspection (most of it originating in studies of large industrial teams and popularised through inspection tooling vendors) reports two consistent findings, and they have been quoted so often that it is worth stating them with proper hedging. Defect detection per line falls sharply once a single review passes somewhere in the range of a few hundred lines, and it falls sharply again once a review session runs beyond about an hour. The specific thresholds vary by study, by language and by domain, and the numbers you see quoted are averages over settings that may not resemble yours."}]},{"t":"p","runs":[{"text":"What is not in doubt is the shape. Detection per line goes down as review size goes up, monotonically, in every measurement anyone has made. There is no plateau where a reviewer becomes efficient at scale."}]},{"t":"p","runs":[{"text":"That curve was always true. What changed is that producing a 2,000-line change now takes minutes, so the norm that used to be enforced by the cost of typing has to be enforced deliberately."}]},{"t":"h2","id":"what-a-large-diff-does-to-a-reviewer-mechanically","runs":[{"text":"What a large diff does to a reviewer, mechanically"}]},{"t":"p","runs":[{"text":"It is not laziness. It is capacity."}]},{"t":"p","runs":[{"text":"Understanding a change means holding a model of the before-state and the after-state in mind simultaneously and comparing them. That model has to include the invariants: what was guaranteed before, what is guaranteed now. Every additional file adds to the model, and the models interact, so the cost is superlinear rather than linear."}]},{"t":"p","runs":[{"text":"At some point the model no longer fits, and what a reviewer does then is switch strategy without noticing. They stop comparing states and start pattern-matching on the lines: does this look like reasonable code? That is the exact check that generated code passes trivially. So a large diff does not merely reduce the amount of review. It converts review into the one form of review that no longer works."}]},{"t":"h2","id":"how-to-actually-split","runs":[{"text":"How to actually split"}]},{"t":"p","runs":[{"text":"“Make smaller pull requests” is advice everyone has heard and few follow, because the mechanics are the obstacle. Three that work:"}]},{"t":"p","runs":[{"text":"Separate the refactor from the change.","b":true},{"text":" This is the highest-value split and the easiest to sell. First a commit that moves and renames things with zero behaviour change, verified by the existing tests. Then a commit that changes behaviour, which is now twenty lines instead of four hundred. The reviewer reads the second one properly because it is readable."}]},{"t":"p","runs":[{"text":"Stack the branches.","b":true},{"text":" Branch B off branch A, open both, and note the dependency. Most git hosts show only the incremental diff for the second one. GitHub, GitLab and the "},{"text":"git-town","code":true},{"text":" and "},{"text":"graphite","code":true},{"text":" style tools all support this; so does plain git if you rebase carefully."}]},{"t":"code","lang":"bash","label":"Bash","text":"git switch -c 1-add-currency-column main\n# ... commit ...\ngit switch -c 2-use-currency-in-total 1-add-currency-column"},{"t":"p","runs":[{"text":"Land it dark.","b":true},{"text":" Merge the new code behind a flag that is off, in one reviewable piece, then flip it in a two-line change. The reviewer of the big piece knows nothing is live; the reviewer of the two-line piece knows exactly what changes."}]},{"t":"h2","id":"set-a-number-and-enforce-it-in-ci","runs":[{"text":"Set a number and enforce it in CI"}]},{"t":"p","runs":[{"text":"A soft norm dissolves under deadline. A number in a pipeline does not."}]},{"t":"code","lang":"yaml","label":"YAML","text":"# fail the build on very large diffs, with an explicit override label\n- name: diff size\n  run: |\n    n=$(git diff --numstat origin/main...HEAD \\\n        | grep -vE '(lock|\\.snap|generated/)' \\\n        | awk '{s+=$1+$2} END {print s+0}')\n    echo \"changed lines: $n\"\n    test \"$n\" -lt 500 || test \"${OVERRIDE:-}\" = \"true\""},{"t":"p","runs":[{"text":"Exclude lockfiles, snapshots and generated directories, because they are noise and blocking on them teaches people to ignore the rule. Provide a documented override, because there are real exceptions (a dependency bump across a hundred files, a formatter run, a genuine bulk rename) and a rule with no escape hatch gets disabled entirely within a month."}]},{"t":"h2","id":"the-counter-argument-which-is-real","runs":[{"text":"The counter-argument, which is real"}]},{"t":"p","runs":[{"text":"Splitting has a cost. Five stacked pull requests mean five review cycles, five CI runs, five context switches, and a merge order somebody has to remember. On a team where review latency is already three days, five sequential reviews is more than two weeks for one feature, and that is a worse outcome than one large review."}]},{"t":"p","runs":[{"text":"So the honest rule is conditional. Small diffs pay off when review turnaround is fast. If review takes days, splitting makes delivery worse, and the thing to fix first is the latency, which takes you back to the queueing lesson. The two problems are linked and solving them in the wrong order makes both worse."}]}],"check":{"question":[{"text":"A team introduces a 400-line cap on pull requests. Review latency is currently four days. Six weeks later, feature delivery has slowed. What is the most likely explanation?"}],"options":[{"text":[{"text":"The cap forced people to split changes badly, producing pull requests that cannot be understood in isolation."}],"correct":false,"tempting":[{"text":"A real failure mode of careless splitting, but it would show up as confused reviews rather than as slower delivery overall."}]},{"text":[{"text":"Reviewers are spending the same total time on more, smaller reviews, so the fixed overhead per review now dominates."}],"correct":false,"tempting":[{"text":"Per-review overhead is real, but it is small compared with a four-day wait repeated for each piece of a split feature."}]},{"text":[{"text":"Small diffs only reduce defects, never lead time, so any delivery slowdown must come from an unrelated change in the same period."}],"correct":false,"tempting":[{"text":"Treats the two effects as independent when splitting directly multiplies the number of times a change passes through the queue."}]},{"text":[{"text":"Each split feature now waits in the four-day queue several times over."}],"correct":true,"tempting":null}],"order":[0,2,1,3],"why":null},"prev":{"slug":"the-review-budget","title":"Spending a review budget","minutes":9},"next":{"slug":"reading-the-tests","title":"Reading a test suite you did not write","minutes":10}}}