Engineering Practice
Short-lived branches vs gitflow
A team reads “trunk-based development” and hears “commit straight to main, no branches, no PRs.” They turn off branch protection. Within a week the trunk is red twice a day, an unreviewed change leaks a credential, and they conclude trunk-based “doesn’t scale.” They threw out the wrong thing. Trunk-based was never about deleting branches — it’s about keeping them so short-lived that they never accumulate drift. The branch lives for an hour, behind a green gate and a review, then it’s gone.
What “short-lived” actually specifies
DORA’s definition of trunk-based is concrete, not vibes. Keep three or fewer active branches in the repository at any time. Branches should last no more than a day before merging into trunk — often only a few hours. There are no code freezes and no integration phases. Each branch is one small, scoped change that opens a PR, passes a fast CI gate, gets a quick review, and merges the same day. The branch is a staging area for review and gating, not a place where work lives.
This is sometimes run as pure “commit to trunk” (no branches at all, common in small co-located teams with pairing) and more often as “short-lived feature branches” (the version most teams use, because it keeps code review and the PR gate). Both are trunk-based. The dividing line isn’t “branch or no branch” — it’s lifetime. A branch that lives hours is trunk-based; a feature/* branch that lives two weeks is the long-lived branch the practice exists to eliminate, no matter what you call your workflow.
Why gitflow optimizes for the opposite world
Gitflow gives you permanent main and develop branches, plus feature/*, release/*, and hotfix/* branches with formal phases: features integrate into develop, a release branch stabilizes, then merges to main and tags. It’s a coherent model — for the world it was designed for: shrink-wrapped, versioned software with scheduled releases, multiple versions supported in the field, and no continuous deployment. In that world, an explicit stabilization phase and parallel release lines earn their keep.
For a continuously deployed web service, gitflow institutionalizes exactly the drift the previous lesson warned about. Long-lived feature/* branches accumulate divergence; the develop-to-release-to-main promotion is a built-in integration phase; “done” features sit in develop rotting while they wait for the next release train. You inherit big-bang merges and code freezes by design. DORA’s research explicitly flags integration phases and code freezes as anti-patterns for delivery performance — gitflow makes them load-bearing.
| Dimension | Gitflow | GitHub flow | Trunk-based |
|---|---|---|---|
| Long-lived branches | main + develop (+ release) | main only | trunk only |
| Feature branch life | Days to weeks | Until PR merges (variable) | Hours, < 1 day |
| Integration phase | Explicit (release branch) | None | None |
| Built for | Versioned, scheduled releases | Web apps, continuous deploy | Continuous deploy at scale |
| Release | Promote develop → release → main | Deploy merged main | Tag/branch from trunk at release point |
Releasing without long-lived branches
The objection: “if there’s only trunk, where do releases come from, and how do I patch an old version?” Trunk-based has a specific answer — branch for release, not for feature. When you cut a release, you create a short-lived release/x.y branch from trunk at that commit. Day-to-day development never happens on it. If a critical fix is needed, you fix it on trunk first (so the next release has it too) and cherry-pick the single commit onto the release branch. The release branch is a read-mostly snapshot, not a parallel development line, so it never accumulates drift. Teams doing continuous deployment often skip even this and just tag trunk.
For big changes that genuinely can’t ship in a day — replacing an ORM, swapping a payment provider — the trunk-based tool is branch by abstraction, not a long-lived branch. You introduce an abstraction layer over the thing you’re replacing, build the new implementation behind it incrementally on trunk (each step merged daily, dark until ready), switch the abstraction to the new implementation, then remove the old one and the abstraction. The large refactor lives on trunk the entire time as a sequence of small green commits — never as a three-week branch.
Why this works
The naming trap is real: a feature/* branch in a gitflow repo and a short-lived branch in a trunk-based repo can look identical in git. The difference is invisible in the branch and visible only in its lifetime and the discipline around it. “We use feature branches” tells you nothing about whether you’re trunk-based — “our branches are merged or deleted within a day” tells you everything.
You're replacing the ORM across a continuously deployed service. The migration will take six weeks. How do you do it trunk-based?
What is the actual dividing line between trunk-based and a long-lived-branch workflow?
In trunk-based development, how is a critical fix to an already-released version handled?
Order a branch-by-abstraction migration on trunk:
- 1 Introduce an abstraction layer over the component you're replacing
- 2 Build the new implementation behind the abstraction in small daily-merged commits, dark
- 3 Switch the abstraction to point at the new implementation
- 4 Verify in production, then delete the old implementation
- 5 Remove the now-redundant abstraction layer
- 01A team turned off all branch protection because they read that trunk-based means 'no branches.' What did they misunderstand, and what does trunk-based actually specify?
- 02If there's only trunk, how do you cut releases and do a six-week refactor without a long-lived branch?
Trunk-based development forbids long-lived branches, not branches themselves — DORA specifies three or fewer active branches, a lifetime under a day, and no integration phases or code freezes. Whether you commit directly to trunk or use short-lived feature branches with PRs, the distinguishing variable is lifetime, not the presence of a branch. Gitflow’s main/develop/release/hotfix structure is coherent for shrink-wrapped, scheduled, multi-version software, but for a continuously deployed service it institutionalizes the very drift, big-bang merges, and integration phases that trunk-based exists to remove. Releases come from short-lived branches cut from trunk, with fixes made on trunk first and cherry-picked down so the release branch never becomes a parallel development line. And changes too big to ship in a day use branch by abstraction — an abstraction layer, the new implementation built behind it in small daily commits, a flip, then cleanup — so even a six-week refactor lives on trunk as small green steps instead of a long-lived branch.