Merge Hell Has a Name (and a Cure)
Table of Contents
The Same Two Symptoms, Every Time
Every GitFlow-style project I’ve worked on eventually produces the same two symptoms: merge conflicts that take a full day to untangle, and pull requests so large that “LGTM” is the only honest review anybody can give. For years I filed this under bad luck or weak discipline — someone should have merged sooner, someone should have split the ticket.
That framing is wrong, and I only worked out why this week. The problem isn’t the people. It’s a measurable property of the workflow, and it has a name that comes from outside software entirely.
The Name Is Batch Size
Batch size is the amount of work that accumulates between integration points. It comes from Lean manufacturing and queueing theory, reached software through Donald Reinertsen’s work on product development flow, and was popularized for our industry by Accelerate.
The claim is simple: the larger the batch, the more the surrounding system has moved by the time you integrate it — so variance and risk at the integration point grow with batch size, and they grow fairly predictably.
In version control, branch lifetime is a direct proxy for batch size. A branch left open for two weeks has accumulated two weeks of divergence from trunk. The conflicts aren’t misfortune; they’re the arithmetic of the workflow. Merge hell is just what a large batch feels like from the inside.
That reframing matters because it changes what you do about it. “Be more careful when merging” is advice aimed at the symptom. “Reduce the batch” is aimed at the cause.
Trunk-based Development Is That, Applied
Trunk-based development is small batch size expressed as a branching policy: everyone works off main, branches live hours rather than weeks, everything merges at least daily.
The part that usually goes unsaid is that TBD doesn’t run on discipline alone. It has prerequisites:
- Automated tests fast and thorough enough that merging daily doesn’t mean breaking trunk daily.
- Feature flags, which decouple deployment from release — that’s what lets an unfinished feature sit in main without reaching users.
- Fast builds. If the build takes an hour, people will merge less often no matter what the policy says.
Skip those and “just merge daily” becomes a slogan that makes things worse. TBD correlates with high deployment frequency and low change failure rate in the DORA data, but that correlation runs through the prerequisites, not around them.
The Obvious Objection
Fine for a two-day feature. What about a change that genuinely takes weeks — swapping a persistence layer, migrating a schema, replacing a service?
This is where the idea earns its keep, because two well-documented patterns do to a large change exactly what trunk-based development does to git history: cut it into small, safe, independently mergeable pieces, so no long-lived branch is ever needed.
Branch by Abstraction
Originated by Paul Hammant, popularized under this name on Martin Fowler’s bliki.1 Four phases:
- Create the abstraction — put an interface between callers and the existing implementation. Behavior unchanged, trivially mergeable, verifiable entirely on its own.
- Build the new implementation behind it — merged into trunk in small daily commits, unused until it’s wired in.
- Migrate callers incrementally — route callers to the new implementation one at a time. Old and new coexist throughout.
- Remove the old implementation — and often the abstraction along with it, once it stops earning its keep.
0. Before Callers ──▶ OldImpl
1. Wrap Callers ──▶ [Interface] ──▶ OldImpl
2. Build new Callers ──▶ [Interface] ──▶ OldImpl
└──▶ NewImpl (unused)
3. Migrate Callers ──▶ [Interface] ──▶ OldImpl (shrinking)
└──▶ NewImpl (growing)
4. Remove old Callers ──▶ NewImpl
Every phase is small and independently mergeable. A migration that takes a month never requires trunk to leave a releasable state — which is the whole reason this is compatible with trunk-based development in the first place.
Parallel Change (Expand and Contract)
The sibling pattern, for backward-incompatible interface and schema changes. Documented as a refactoring strategy by Joshua Kerievsky in 2006, written up by Danilo Sato on Fowler’s bliki.2 Three phases: expand, migrate, contract.
The detail I found most useful is that writes and reads move asymmetrically:
- Writes go dual immediately, in the expand phase, and stay dual until the very end. They’re cheap to duplicate, and they’re the safety net.
- Reads are what actually migrates, incrementally, and only once the data is provably consistent — backfill the history in small batches, verify old and new agree, then cut readers over behind a feature flag.
That asymmetry is what makes the whole thing revertible. Because the old form never stops being kept current, rolling back a bad read cutover costs nothing. 🛟
The Line Underneath All of It
Three techniques, one principle — and it’s a line from Kent Beck I haven’t stopped thinking about since:
“For each desired change, make the change easy (warning: this may be hard), then make the easy change.”
What it separates is structural change — rearranging code so a new capability becomes simple to add, with no observable behavior difference — from behavioral change, the actual feature or fix. Doing both in one step makes the result harder to review, harder to test in isolation, and harder to attribute when something breaks.
Branch by abstraction is a direct instance of it: phase 1 is “make the change easy,” a pure-structure move that changes nothing observable and can be merged and verified by itself. Phases 2–4 are the easy change it enables.
As a working rule: never let “restructure” and “new behavior” land in the same commit.
The Version of This That Kept Me Up
Here’s where it stopped being a git post for me.
Batch size isn’t only about code accumulating between integrations. It applies just as cleanly to decisions accumulating between points of evidence. A batch of decisions committed all at once — scope, schedule, and design signed off before implementation starts — is a large batch in exactly the same mechanical sense, and it carries exactly the same disproportionate risk.
Which is what Big Design Up Front is, structurally: one enormous decision batch, integrated against reality exactly once, at the end.
Seen that way, batch size turns out to be the lever behind three ideas I’d always kept in separate drawers:
- The cone of uncertainty says decisions made early are made at maximum variance. Small batches move each decision rightward, into a narrower part of the cone.
- The last responsible moment says defer commitment until an option would be lost. But deferral only pays off if evidence arrives in the meantime — which requires small batches to produce it.
- The iron triangle becomes tractable when scope is the corner that absorbs uncertainty, and that’s only possible if scope is decided in increments rather than fixed up front.
Almost every argument I’ve sat through about delivery method — waterfall against agile, phase gates against continuous flow — reduces to two variables: how big the batch is, and how long a decision is held before it’s committed. The activity lists on either side are nearly identical.
I went looking for why my branches hurt, and came back with a way to think about planning. Worth the rabbit hole. 🧩
comments powered by Disqus