Ch. 16 · Git & CI/CD

Git Branching Strategies and Integration Frequency

GitFlow, GitHub Flow and trunk-based development compared by what matters: how long code waits before it meets everyone else's changes.

~6 min readintermediateupdated Oct 7, 2026

“Which branching strategy does your team use, and why?” sounds like a question with a right answer. It isn’t. Interviewers ask it to see whether you can connect a process to the problem it solves. Naming GitFlow and drawing its five branch types from memory scores fewer points than explaining why a team shipping a web app ten times a day shouldn’t use it, and why a team maintaining three major versions of a library might.

The thread that ties every strategy together is integration frequency: how long a change lives on its own before it’s combined with everyone else’s work. The longer that gap, the more the two sides drift, and the more expensive and risky the eventual merge becomes.

Why drift is expensive, in one small repository

Two branches start from the same commit. On main, someone renames calcTax to computeTax and updates every caller that exists. On feature/refunds, someone else writes a new file that calls calcTax. Neither touched the other’s lines.

$ git log --oneline --graph --all
* 3eb65c5 Add refunds
| * 3423397 Rename calcTax to computeTax
|/
* e8ada47 Add tax and invoice
$ git rev-list --left-right --count main...feature/refunds
1	1
Text

That second command is a handy way to measure drift: one commit on main that the feature branch doesn’t have, and one the other way. Now merge:

$ git merge --no-edit feature/refunds
Merge made by the 'ort' strategy.
 refund.js | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 refund.js
Text

No conflict. Git compares lines of text, and no line was changed on both sides. But the code is broken:

$ node refund.js
file:///.../refund.js:1
import { calcTax } from './tax.js';
         ^^^^^^^
SyntaxError: The requested module './tax.js' does not provide an export named 'calcTax'
Text

This is a semantic conflict, and it’s the cost that long-lived branches hide. With one commit of drift it takes a minute to fix. With three weeks of drift, a dozen renames and a schema change, the merge is a project of its own, and the person doing it has to understand changes they didn’t write. A common misconception in interviews is that “merge conflicts” are the problem with long branches. Textual conflicts are the visible part. The breakage that merges cleanly is worse, because only tests catch it.

GitFlow: built for versioned releases

GitFlow, described by Vincent Driessen in 2010, has two permanent branches, main (what’s released) and develop (what’s next), plus three kinds of temporary ones: feature/* off develop, release/* to stabilise a version, and hotfix/* off main for urgent fixes that get merged back into both.

It solves a real problem: shipping a numbered release on a schedule while development continues. The release branch freezes the scope for 1.5 while develop moves on to 1.6. Desktop software, mobile apps going through store review, firmware and on-premises products work this way.

The cost is integration delay at every layer. Features wait on their branch until they’re merged into develop. develop waits until a release branch is cut. Hotfixes have to land in two places, and forgetting the second merge is how a bug that was fixed in 1.5.1 comes back in 1.6.0. Driessen himself added a note in 2020 saying that teams doing continuous delivery should consider something simpler.

GitHub Flow: one long-lived branch

GitHub Flow drops develop and release branches. main is always deployable. You branch from it, open a pull request, get review and green checks, merge, and deploy. That’s the whole model.

It works well when there’s one production version, such as a web service. The weak point is branch lifetime, which the model doesn’t limit. A team can follow GitHub Flow perfectly and still have pull requests that sit open for two weeks, at which point it has the same drift problem as GitFlow with less ceremony. The process only helps if branches stay small.

Trunk-based development: integrate daily or more

In trunk-based development, everyone integrates into main (the trunk) at least once a day, either by committing directly or through short-lived branches that last hours. Unfinished features are merged behind feature flags rather than kept on a branch. Releases are cut from the trunk, sometimes as a tagged commit, sometimes as a release branch that only receives cherry-picked fixes.

This keeps drift near zero, so the rename above would have reached the refunds author the same afternoon, while they were still writing the file. It also has prerequisites that interviewers will probe:

  • A fast, trusted CI run. If checks take an hour or fail randomly, people batch work up to avoid them, and you’re back to long branches.
  • Feature flags. Merging incomplete work is only safe if it’s switched off. That brings its own discipline: someone has to remove old flags.
  • Small, reviewable changes. Reviewers need to be able to read a pull request in minutes for it to merge the same day.

Saying “we’ll do trunk-based” without these is a mistake. Removing branches doesn’t remove the need for safety; it moves the safety from branch isolation to tests and flags.

Choosing, and what to say about it

A reasonable way to decide:

  • How many versions are live? One production deployment points towards GitHub Flow or trunk-based. Several supported versions (a library with 2.x and 3.x, an app customers update on their own schedule) need release branches that live as long as the version is supported.
  • How fast and reliable is CI? If it can’t give a trustworthy answer within about ten minutes, fix that before shortening branches.
  • Can you ship code that’s turned off? If not, features will live on branches until they’re done.

Release branches aren’t a GitFlow-only idea. Plenty of trunk-based teams cut release/2026.10 from main, ship from it, and cherry-pick fixes onto it. What they avoid is long-lived feature branches and a separate develop.

You can also measure the thing that matters instead of arguing about names. The age of open pull requests, the number of commits a branch is behind main when it merges, and how often main is red are all easy to track and tell you more than which diagram is on the wiki.

Interview practice

Your team uses GitFlow for a single SaaS web app deployed to one production environment. Releases happen every two weeks. Developers complain that merging develop into release branches takes a day, hotfixes sometimes reappear as regressions, and features are often “done” for ten days before users see them. The manager asks whether to switch strategy. What would you recommend, and what would you put in place first?

Answer

GitFlow’s structure exists to support several versions in flight, and this team has one production deployment, so it’s paying the cost without needing the benefit. The three complaints all come from integration delay. The release merge is slow because develop drifts from the release branch. Hotfixes reappear because they must be merged into two branches and one merge gets missed. Features wait ten days because they sit in develop until the next release cut.

I’d move towards GitHub Flow with short-lived branches, and eventually trunk-based development, but not in one step. First, make CI fast and reliable enough that merging to main daily is safe, and add feature flags so unfinished work can merge switched off. Then retire develop, branch from main, and deploy from main more often than every two weeks. Hotfixes become ordinary pull requests to main, so there’s only one place to fix things. Keep a short-lived release branch only if something, such as a compliance sign-off, really requires a frozen candidate. Track the age of open pull requests to check the change worked.

Further reading

More in Git & CI/CD

esc