Branching strategy diagrams all look reasonable on a whiteboard. The real test is what a strategy costs your team on an ordinary Tuesday — how much ceremony it takes to ship a small fix, and how often it causes a merge conflict nobody wanted. Here's how the three most common approaches actually play out.

Git Flow: built for scheduled releases

Git Flow uses long-lived develop and main branches, plus dedicated feature/, release/, and hotfix/ branches. Work merges into develop, gets batched into a release/ branch for stabilization, and only then reaches main.

What it's good at: software with scheduled, versioned releases — desktop apps, embedded firmware, anything shipped in discrete builds rather than continuously deployed. The structure gives you a clear place to stabilize a release without freezing all future work.

What it costs: overhead. Every change passes through several branches before reaching production, feature branches can live for weeks, and the longer a branch lives, the more painful its eventual merge tends to be. For a team deploying multiple times a day, this ceremony mostly gets in the way.

GitHub Flow: built for continuous deployment

GitHub Flow simplifies this down to one long-lived branch, main, plus short-lived feature branches that merge back in via pull request as soon as they're reviewed and tested. There's no separate develop branch and no scheduled release branch — main is always deployable.

git checkout -b fix/checkout-bug
# make changes, commit
git push origin fix/checkout-bug
# open a pull request, get it reviewed, merge to main
# main deploys automatically

What it's good at: web apps and services deployed continuously, where "release" isn't a scheduled event but something that happens dozens of times a week. Fewer branches mean fewer places for divergence to build up.

What it costs: it assumes you have solid automated testing and a comfortable deploy pipeline, because there's no dedicated stabilization phase. Teams without strong CI can end up shipping half-tested changes straight to main.

Trunk-based development: built for speed and small changes

Trunk-based development pushes this further: everyone commits directly to main (or merges via very short-lived branches that live hours, not days), behind feature flags when a change isn't ready to be user-visible yet. Instead of a branch hiding unfinished work, a flag does.

if (featureFlags.isEnabled('new-checkout-flow')) {
  return renderNewCheckout();
}
return renderLegacyCheckout();

What it's good at: larger teams working on the same codebase, where long-lived branches would otherwise create painful, high-conflict merges. It also enables true continuous integration — everyone's code is tested against everyone else's changes constantly, not just at merge time.

What it costs: discipline and tooling. It requires a feature-flagging system, a strong testing culture, and comfort with the idea that "merged" doesn't mean "visible to users." Without those pieces already in place, trunk-based development can feel chaotic rather than fast.

A rule of thumb Team size and deploy frequency predict this better than anything else. Small team, deploying continuously → GitHub Flow. Large team, deploying continuously, willing to invest in feature flags → trunk-based. Scheduled, versioned releases → Git Flow still earns its keep.

The mistake to avoid

The most common real-world failure isn't picking the "wrong" strategy — it's picking one and then not actually following it. A team nominally doing GitHub Flow but letting feature branches live for three weeks has, in practice, reinvented Git Flow's slow merges without any of its structure to manage them. Whichever model you choose, the branch lifetime you actually enforce matters more than the name on the diagram.