Long-Lived Branches
Divergence has a cost that grows superlinearly with time, and the expensive part — semantic conflict — is invisible to every merge tool.
The question, the obvious approach, and why it breaks
Every lesson starts where the work starts: an operational problem, a first attempt that is entirely reasonable, and the way production disagrees with it.
What does a branch actually cost per day it stays open, and when is that cost worth paying?
A branch feels free. Nothing degrades visibly while it is open, the diff grows quietly, and the cost is paid all at once at merge time — which is also the moment everyone wants it to be finished.
Keep the feature on its own branch until it is complete, so main stays clean and the work is reviewed as a coherent whole.
Textual conflicts grow with the amount of overlapping change, and resolving them means reasoning about two intents at once, days after either was fresh.
- Textual conflicts grow with the amount of overlapping change, and resolving them means reasoning about two intents at once, days after either was fresh.
- Semantic conflicts merge cleanly and are wrong. The branch adds a caller to a function while main changes that function's contract; Git sees no conflict and the tests only catch it if a test exercises that combination.
- Review quality collapses at large diffs. A four-thousand-line change gets structural comments and a quick approval, which is the observable signature of review having stopped working (Review as a Gate).
- The branch is deployed for the first time on the day it merges, so every deployment risk in weeks of work arrives as one event with one rollback (Change Size: Why Small Changes Are Safer, and When They Are Not).
- Two long-lived branches touching the same area produce a conflict between two people's reconstructions of what the code was supposed to do, resolved by whoever merges second.
- Dependency and environment drift: the branch was built against a base image and a lockfile that main has since moved past, so the merge changes more than the diff shows (Environment Drift).
What is actually happening
Underneath the tooling, which is the part that survives a change of tool.
- A branch is a claim that the world will not change underneath it. The claim gets weaker every day, and the cost of it being wrong is paid entirely at merge.
- Textual conflict is the cheap kind: the tool detects it, a human resolves it, and the resolution is reviewable. It scales roughly with overlapping edits.
- Semantic conflict is the expensive kind. Both sides are individually correct, the merge is clean, and the combination is wrong. No merge tool detects it, review rarely does because the reviewer is looking at one side, and only a test that exercises the combination will — if one exists.
- Rebasing keeps a branch current textually and does nothing about semantic drift. It also rewrites the branch, which is fine on a private branch and destructive on a shared one (Protected Branches).
- The deployment cost is separate and often larger: an unmerged branch is unverified in production. Weeks of change that arrive in one deploy have one blast radius and one rollback, and bisecting to find the culprit is not available.
- Not all long-lived branches are the same object. A release branch is long-lived *by design*, carrying only backported fixes, with divergence that is bounded and intentional. A feature branch that has been open for three weeks is unbounded divergence that nobody chose.
What accumulates, day by day
The reason branches feel free is that nothing happens visibly while they are open. This is what is actually accruing, and it is why the cost curve bends rather than being linear.
- Day 0changeBranch created from main. Divergence zero; merging is a fast-forward
- Day 2changeMain has moved: two unrelated changes. Still no conflicts
- Day 5changeMain refactors a helper the branch calls. Merge is still clean — the call site is unchanged, its contract is not
- Day 8signalFirst textual conflict on a shared file. Ten minutes to resolve, still cheap
- Day 12changeDependencies updated on main. The branch is now built against a different lockfile
- Day 15changeAnother team starts a branch touching the same module. Two divergent reconstructions now exist
- Day 18signalDiff reaches roughly four thousand lines. Review will be structural at best
- Day 19actionRebase onto main: fourteen conflicts, resolved by someone reasoning about two intents at once
- Day 19signalSuite fails on the merged result for reasons neither side introduced alone — the day-5 contract change
- Day 20changeMerged and deployed. Eighteen days of change reach production in one event with one rollback
- Day 22signalDefect reported. Suspect list is the whole merge; bisect is unavailable because it landed as one unit
The genuinely expensive moment is Day 5, and nothing observable happens on Day 5. That is the whole lesson: the cost is incurred silently and billed at merge.
Two kinds of conflict
Almost all tooling addresses the cheap kind. Understanding why the expensive kind is undetectable is what makes the case for short branches concrete rather than stylistic.
1main, day 5:2 charge(amount) -> charge(amount, currency)3 every existing caller updated to pass currency4 5branch, day 6 (created day 0, never integrated):6 new caller added: charge(total)7 correct against the contract the branch was written for8 9merge, day 19:10 no textual conflict - the two sides touched different lines11 no review objection - the reviewer sees one side at a time12 result: one caller passes no currency13 caught only if a test exercises that specific new pathGit compares text. Neither side edited the other's lines, so there is nothing for it to flag. The defect is created by the merge itself, which is the one artifact nobody reviews.
When a long-lived branch is the right call
The failure mode is unbounded divergence that nobody decided on. A branch that is long-lived by design, with a narrow and controlled scope, is a different object and is often correct.
A substantial change cannot be finished in a day. What is the right structure for it?
when The change can be decomposed into independently-correct steps — most feature work can.
cost Requires flags, slicing effort, and a codebase carrying an inactive path until cleanup (Feature Flags: Deploy Is Not Release).
when Replacing an implementation where a seam can be introduced first.
cost The abstraction is extra code, and it must be removed afterwards or it becomes permanent scaffolding.
when A sweeping mechanical change — a framework upgrade touching every file — that cannot be usefully sliced.
cost Everyone else's branches conflict with it; needs an agreed window and a fast merge.
when Supporting a version customers are running while development continues on main.
cost Permanent backport work and an end-of-life policy someone owns (Git Workflows).
when Rarely defensible. Occasionally the honest answer for genuinely exploratory work not intended to merge as-is.
cost Everything in this lesson. If it is exploratory, say so and expect to rewrite rather than merge.
How to do it properly
Most important first.
- Set a maximum branch age and treat exceeding it as a signal to slice, not as a scheduling problem (Plan and Code).
- Integrate from main into the branch daily so textual conflicts stay small — accepting that this does nothing for semantic drift.
- For work too large to finish quickly, merge unfinished increments behind a flag instead of holding them on a branch (Feature Flags: Deploy Is Not Release).
- For invasive structural change, use branch by abstraction: introduce the seam, add the new implementation behind it, switch, remove the old. Every step merges.
- Keep release branches deliberately narrow: only backported fixes, never new development, with an owner and an end-of-life date (Git Workflows).
- When a long branch is unavoidable, deploy it somewhere real before merge day — a preview environment reduces how much risk is discovered at merge (Preview Environments).
How much can this affect
Every production change has a blast radius. Stated as a scale so it is comparable between changes rather than adjectival — and paired with what actually contains it, because a wide scope with a real containment mechanism is a different situation from a wide scope with none.
Nothing, once merged and deployed as one event. That is the core problem — weeks of change with one deployment and one rollback, and no way to bisect.
What can go wrong
- The merge that "just needs conflict resolution" and takes three days, because resolution requires re-deciding design questions from two weeks ago.
- A semantic conflict that survives the merge, passes review, ships, and manifests as a production defect with no obvious suspect (Change Correlation).
- A branch abandoned as too painful to merge, discarding the work entirely — the cost realised as a total loss.
- Release branches that accumulate divergence because a fix was made directly on the branch and never applied to main, so it reappears in the next release.
- A branch rebased and force-pushed while someone else has it checked out, producing two versions of the same work.
- Daily merges from main creating a merge-commit history so noisy that bisecting the branch becomes impractical.
- "Rebasing keeps my branch up to date." Textually, yes. The assumptions your branch relies on can change without producing a single conflict.
- "A clean merge means a safe merge." A clean merge means no overlapping text. It says nothing about whether the combined behaviour is correct.
- "Long-lived branches are always wrong." Release branches are long-lived by design and are correct wherever supported versions exist. The problem is unbounded, unintentional divergence.
- "The branch is fine, it is main that keeps changing." Main changing is the normal state. A branch is a bet against it.
- "We will fix conflicts at the end." The end is when everyone most wants it merged and least wants to redesign anything, which is the worst possible time to be resolving design questions.
Operating it
Evidence is the signal, not the intention. Rollback is sometimes 'you cannot, and that is the point'.
- Branch age distribution is visible, and the long tail is small and explained.
- Merges requiring manual conflict resolution are rare enough that each one is remembered.
- For each release branch you can name its owner, its end-of-life date and its last backport.
- No fix exists on a release branch that is not also on main — checkable, and worth checking.
- Post-incident, changes can be attributed to a specific small commit rather than to "the big merge".
- Reverting a large merge is technically simple and semantically messy: a revert commit of a big merge removes a lot of work at once, and re-landing it later is its own project.
- Production rollback is unaffected by branch structure — redeploy the previous artifact digest (Rollback: Only Useful If It Is Actually Safe).
- A branch abandoned is unrecoverable in practice even though the commits still exist, because the world it was written against has moved.
- You cannot roll back a semantic conflict that has already shipped; you can only find it, which is why bisectability of small commits is worth so much (Change Size: Why Small Changes Are Safer, and When They Are Not).
- Automate the visibility: branch age reporting, divergence counts, and warnings when a branch passes an agreed age.
- Automate backport mechanics for release branches, and automate the check that every release-branch fix also exists on main.
- Keep human: deciding when a branch should be sliced, and resolving conflicts — semantic resolution requires knowing what both sides meant (The Automation Trap).
- Short branches mean unfinished work in main, which needs flags or abstractions and produces a codebase with more inactive paths.
- Daily integration from main into a branch costs interruption and adds merge noise, and it only addresses the cheap half of the problem.
- Release branches are long-lived on purpose and are worth their cost when customers control their own upgrades — the correct answer there, not a compromise (Git Workflows).
Where this applies
This domain is unusually tool- and organisation-dependent. These labels say what each claim is specific to, and what a different platform, provider or organisation does instead.
- GENERALDivergence cost is a property of concurrent editing and applies to any version control system. Git makes textual merging unusually cheap, which slightly increases the temptation and does nothing about semantic divergence.
- SCALE-SPECIFICWith two people in one file, a week-long branch is nearly free. The cost scales with how many people are changing related code: on a repository with many contributors touching shared modules, a branch open for a week is already expensive, and one open for a month is usually re-written rather than merged.
Where the depth lives
This domain teaches delivery and operation, and hands the mechanism off to the domain that owns it.
- — Testing & Reliability Engineering — a test that exercises the combination is the only automated defence against a semantic conflict.