Skip to content
Practitioner9 min readUpdated September 2026

Trunk-Based Development And Why It Matters

Branching strategy is a batch-size decision with organisation-wide consequences, not a matter of developer taste. Why merge cost rises with branch age, what replaces the long-lived branch, and what must be true before trunk-based development works.

Branching strategy is usually discussed as a matter of engineering preference, somewhere between editor choice and tabs versus spaces. A team adopts a model, puts a diagram on the wiki, and the conversation ends. Nobody outside the engineering group has an opinion, because the topic appears to be internal plumbing with no bearing on anything a business cares about.

This is a category error, and an expensive one. Branching strategy determines how long work sits unintegrated, which determines integration batch size, which determines merge cost, defect discovery latency, the amount of rework in the system and — through all of those — how quickly the organisation can change its mind. It is one of the few purely technical decisions that reliably shows up in delivery metrics at a portfolio level.

The argument for trunk-based development is not that it is modern or that the good teams do it. It is that long-lived branches are inventory. They are finished-looking work held in a state where it cannot be validated, cannot be released, and grows more expensive to integrate every day it remains. Everything that is true of large release batches is true of large integration batches, for exactly the same reasons and usually sooner.

Merge cost is a function of branch age

The intuition most engineers carry is that merge difficulty depends on how much code the branch changed. That is part of it, but it is the smaller part. The dominant variable is how much trunk changed underneath the branch while it was away.

A branch that lives for four hours diverges from a trunk that has barely moved. A branch that lives for three weeks diverges from a trunk that has absorbed dozens of other people's changes, several refactorings, a dependency upgrade and possibly an architectural decision that invalidates the branch's assumptions. The conflict surface is the product of two quantities that both grow with time, which is why merge pain does not rise linearly with branch age. It rises roughly with the square of it.

There is a second cost that does not appear as a conflict at all, and it is the one that causes outages. Semantic conflicts — where two changes merge cleanly but are incompatible in behaviour — are invisible to version control. A branch that renames a concept and a branch that adds a caller of the old concept will merge without complaint and fail at runtime. Automated tests catch some of this. Nothing catches all of it, and the probability rises with every day of divergence.

GitFlow is a batch-size problem wearing a diagram

GitFlow deserves a fair hearing, because it was a reasonable answer to a real problem. It was designed for software with explicitly versioned releases shipped to consumers who upgrade on their own schedule — desktop applications, libraries, embedded firmware. In that context, long-lived release branches and hotfix branches for supported versions are not overhead; they are the shape of the product.

The failure is contextual. Most teams that adopted it are building a continuously deployed web service with exactly one version in production, and for them the model adds branch types with no corresponding need. A develop branch that is not releasable, feature branches that live for the duration of a feature rather than the duration of a change, and release branches that exist to hold a stabilisation period that should not exist — each of these is a queue, and each holds work in a state where it is neither integrated nor delivered.

The tell is the language teams use. When people describe "merge week", "integration hell", or a code freeze before a release, they are describing the symptoms of a batch problem and attributing them to volume of work. They are not the same thing. Two teams writing identical code, one integrating hourly and one integrating fortnightly, will experience wildly different amounts of pain, and only one of them will be able to answer the question "does the current state of the system work?" at any given moment.

Trunk-based development, stated plainly, is: everybody commits to one shared branch, short-lived branches are permitted but measured in hours rather than days, the trunk is always releasable, and unfinished work is hidden by technique rather than by isolation in version control.

That last clause is where the real work lives, and it is where most adoption attempts fail. If you remove long-lived branches without replacing what they were doing, you will simply push half-finished work into production. The branch was providing something. You have to provide it another way.

What replaces the branch

Three techniques do almost all the work. They are not exotic, but they require deliberate design rather than a policy announcement.

Feature flags. The most direct substitute. Code reaches trunk and production continuously while the user-visible behaviour stays off until it is ready. This separates deployment from release, which is the single most useful decoupling available in software delivery, and it makes the release decision a business decision taken at a keystroke rather than an engineering event requiring a window. The discipline that matters is removal: every flag is a branch in the runtime, and a codebase with hundreds of stale flags has recreated its merge complexity in a place where the compiler cannot help. Treat flag removal as part of finishing the work, not as tidying to be done later.

Branch by abstraction. For changes too large to hide behind a simple flag — replacing a persistence layer, swapping a payment provider, moving off a framework. Introduce an abstraction over the existing implementation, migrate callers to the abstraction, build the new implementation behind the same abstraction, switch over, then remove the old path and often the abstraction itself. Every step is a small, safe, independently releasable commit on trunk. The migration takes the same total effort and carries a fraction of the risk, because at no point does an unreleasable state exist.

Expand and contract. The database and API equivalent. Add the new column or field without removing the old one, write to both, migrate readers, then remove the old one in a later change. Each step is backwards compatible, which means each step can be deployed and rolled back independently. Schema changes are the usual reason teams claim they cannot deploy small increments, and expand-and-contract is the answer to almost all of it.

The common thread is that all three replace isolation in version control with isolation at runtime or in the type system. That is a better place to put it, because runtime isolation can be tested, observed and reversed, whereas a branch can only be merged or abandoned.

What has to be true first

Trunk-based development is not free, and teams that adopt it without the supporting conditions get a worse outcome than they started with. Be honest about these before changing policy.

A test suite that can be trusted to gate the trunk. Not comprehensive — trusted. If a green build does not meaningfully increase your confidence that the system works, then committing to trunk more often just means breaking it more often. Flaky tests are disqualifying, because the team will learn to ignore red, and at that point the trunk is no longer a signal. This is the dependency that most often needs work first, and it is covered in test strategy for teams that ship daily.

Review latency measured in hours. If pull requests wait a day and a half for attention, short-lived branches are arithmetically impossible regardless of intent. Review latency is a queueing problem, not a diligence problem, and it usually responds to a work-in-progress rule — review before you start something new — far better than to encouragement. Pairing or ensemble work removes the queue entirely by making review synchronous.

A pipeline fast enough to run on every commit. If the build takes fifty minutes, developers will batch their commits, and you will have long-lived branches under another name. Pipeline duration is a first-class engineering concern that deserves real investment; a build that finishes in under ten minutes changes behaviour in ways that no amount of process guidance will.

Small, vertically sliced work. A branch is long-lived because the work is large. Splitting stories until they can be completed in a day or two removes the reason for the branch to exist, and it does more for integration frequency than any tooling change.

A revert path you have actually used. Teams commit confidently to trunk when reversing a mistake is cheap and unremarkable. If rolling back requires a call with three people and a database restore, nobody will move quickly, and they are right not to.

The objections, answered honestly

ObjectionThe honest answer
Our reviews would be rushedReviews get better on small changes, not worse. A three-hundred-line diff gets real scrutiny; a three-thousand-line diff gets approved
We need branches for code quality gatesGates run on trunk commits just as well. What you lose is the ability to block a merge, which mostly delayed the discovery
Unfinished work would reach productionOnly if you skip the replacement techniques. Flags and abstractions exist precisely for this
We have regulatory separation requirementsPeer review on every trunk commit plus an immutable pipeline record generally satisfies separation of duties better than a branch does. See agile in regulated environments
Our codebase is too coupledTrue, and it is an argument for fixing the coupling, not for hiding it in branches where it compounds unobserved
It works for small teams onlyLarge numbers of engineers have worked this way on very large monorepos for decades. Scale argues for trunk-based, not against it, because divergence cost grows with the number of concurrent contributors

The one objection with genuine weight concerns open-source style contribution from parties who cannot be given commit access, and products that genuinely support multiple concurrent released versions. Those are real, and they call for real release branches. They are also much rarer than the number of teams currently using them.

What to do on Monday

Pull the branch-age distribution for the last thirty days. If the eighty-fifth percentile is more than two days, you have a measurable integration batch problem and a target to move.

Then pick the single largest contributor to that tail and ask why it was long-lived. It will be one of four things: the work item was too big, the review queue was slow, the pipeline was slow, or there was no technique available for hiding unfinished behaviour. Each has a specific remedy and none requires a strategy document.

Set one rule with the team this week: no branch survives a night. Not as a value, as a constraint, with the explicit understanding that anything that cannot comply is revealing a structural problem worth naming. Keep the list of things that could not comply. That list — not the policy — is the actual work, and it is almost always shorter than people expect before they write it down.