Shipping faster with smaller diffs

/4 min read

Why I optimise for review surface area instead of lines of code.

Review surface area, not line count

Most teams I have worked with measure the size of a change in lines. I stopped doing that a while ago, because line count is a poor proxy for what actually slows a pull request down. The real cost is review surface area: the amount of system a reviewer has to hold in their head to say yes with confidence. A two hundred line diff that adds one well-isolated module is cheap to review. A twenty line diff that touches the auth middleware, a shared hook, and a database migration is not.

Surface area is about coupling, not volume. Every file a change touches drags its dependents into the review. If I modify a utility used in forty places, the honest review covers all forty call sites, whether or not the reviewer actually opens them. Most reviewers will not, which is how regressions slip through with an approval attached. So when I plan a change, the question is not how many lines it will take but how many existing behaviours it can plausibly break. I try to drive that number towards one.

This reframing changed how I write code, not just how I split it. Architecture that keeps surface area small is architecture with clear seams: modules that own their data, components that take props instead of reaching into context, server logic behind narrow interfaces. When the seams are right, most features land as additive diffs. When every feature requires editing five shared files, that is not a discipline problem to fix with smaller pull requests. It is a signal that the shape of the system is wrong, and the refactor should come first.

Slicing work into reviewable units

A feature is rarely one change. Building the call logging screen for a tele-sales CRM, the work split naturally into a schema migration, a data access layer, the UI, and the wiring into the dialler. Shipped as one pull request, that is a day of someone else's attention. Shipped as four, each one is reviewable over coffee. The trick is that the slices have to be independently correct, not just independently small. Each one must leave the system working, even if the feature it serves is not visible yet.

I slice from the bottom up. The migration and the types go first, because everything else depends on them and they are the riskiest part to get wrong. Then the pure logic, which a reviewer can verify by reading the tests. The UI comes last and is usually the largest diff by line count but the smallest by surface area, since it only consumes interfaces already agreed in earlier reviews. By the time the final pull request opens, the only real question left is whether the screen looks and behaves right.

Refactors get their own pull requests, always. If a feature needs a shared component reshaped, I land the reshape first with behaviour unchanged, then land the feature on top. Mixing the two forces the reviewer to untangle which lines are mechanical and which carry intent, and that untangling is where reviews stall for days. A pure refactor with green tests gets approved in minutes because there is exactly one question to answer. The feature diff that follows reads like a feature, not an archaeology dig.

Tooling that keeps diffs small

Formatting noise is the cheapest surface area to eliminate, so I eliminate it completely. Biome runs in a pre-commit hook and CI, which means no review ever contains a whitespace change or an import reorder that someone has to scan past. Strict TypeScript does similar work at a deeper level: when the compiler guarantees a refactor is mechanically complete, the reviewer can trust the types and read only the intent. Every check that runs before the pull request opens is a question the human never has to ask.

Stacked branches are the other half. Git makes it awkward to have four dependent pull requests open at once, but the awkwardness is worth absorbing because it is what makes bottom-up slicing practical. I keep each branch rebased on the one below it and merge from the bottom as approvals land. Feature flags cover the cases where a slice has to ship dark: the code merges, deploys, and sits behind a boolean until the last piece arrives. Merging early and toggling late beats a long-lived branch every time.

Where small diffs don't help

Some changes resist slicing, and pretending otherwise makes them worse. Swapping an ORM, migrating a build system, or renaming a concept that appears in three hundred files all produce diffs that are huge and should stay huge. Splitting a mechanical rename across ten pull requests creates ten windows where the codebase is inconsistent, and the inconsistency causes more confusion than the big diff ever would. For these I write a script, run it in one commit, and ask the reviewer to check the script rather than the output.

The other failure mode is slicing so thin that the review loses the plot. A pull request that adds an interface nobody implements yet is unreviewable in a different way: the reviewer cannot judge whether the abstraction is right without seeing a caller. I aim for slices that each contain one complete decision, not one file. Small diffs are a means of keeping the reviewer's confidence high, and the moment they start lowering it, the optimisation has inverted. Surface area is the target. Line count was only ever a shadow of it.