Skip to content

How to Choose a Git Branching Strategy

Lesson 8 of 8Intermediate14 min readModern Git Workflows · BranchingVerified: Git 2.43.0 on Ubuntu 24.04

There is no best branching strategy. There are strategies that fit a given team, product and release model, and strategies that do not — and adopting one that does not fit produces process nobody follows.

This lesson is a decision guide. It compares the models from this cluster against the factors that actually determine fit, and gives recommendations by scenario rather than a single answer.

Most of the answer falls out of five questions. Answer these honestly before comparing models.

  1. How often do you ship to users? Several times a day, weekly, monthly, or on a schedule set by someone else?

  2. How many versions are in use at once? One (a web service), or several (installed software, libraries, mobile apps in staged rollout)?

  3. Can you undo a bad change quickly? Revert and redeploy in minutes, or does fixing production take hours and coordination?

  4. How good are your automated tests, really? Would you merge a change to main on a green build alone at 5pm on a Friday?

  5. What must happen before code reaches production? Automated checks only, manual QA, or an external approval or certification step?

Questions 2 and 5 mostly determine whether you need release branches. Questions 3 and 4 determine how short your branches can safely be. Question 1 determines whether a formal release process earns its cost.

Feature branchesGitHub FlowGit FlowTrunk-based
Long-lived branchesmainmainmain + developtrunk only
Typical branch lifetimeDaysHours to daysDays to weeksHours
Release mechanismTeam’s choiceMerge to mainRelease branch, taggedDeploy trunk, or branch at release
Hotfix pathAd hocOrdinary branchDefined branch typeOrdinary branch, or revert
Parallel version supportAdd release branchesPoorGoodAdd release branches
Requires strong CIHelpfulStronglyHelpfulEssential
Requires feature flagsRarelyOftenRarelyEffectively yes
Process overheadLowLowHighLow process, high discipline
Integration costGrows with lifetimeLowHighestLowest
Learning curveLowLowModerateModerate

Read the table as describing tendencies. These models are not mutually exclusive — most real teams run a hybrid, and the last section of this lesson covers the combinations that work.

One to three people. Process overhead dominates. Feature branches with light review, or committing directly to main with discipline. Git Flow is almost certainly wrong.

Four to fifteen. The sweet spot for GitHub Flow. Enough people that review and branch protection matter; few enough that coordination is easy.

Fifteen to fifty. Integration frequency starts to bite. Merge conflicts on main become routine and trunk-based practices — smaller changes, faster review, possibly a merge queue — pay off.

Fifty and above. Requires deliberate investment: merge queues, per-path review rules, feature flags, and often splitting the repository or adopting monorepo tooling. The branching model matters less than the automation around it.

Several times a day. Trunk-based development or GitHub Flow. A release-branch stabilisation phase is incompatible with this cadence.

Weekly. GitHub Flow works. A short release branch is optional and often unnecessary.

Monthly or quarterly. A release branch earns its cost. There is a real freeze period during which development must continue.

On a schedule you do not control — app store review, customer maintenance windows, regulatory release trains. Release branches, and possibly maintenance branches.

SaaS, one version in production. The simplest case. One long-lived branch, tags for releases if you want them. No maintenance branches.

Packaged or installed software. Customers run several versions. You need maintenance branches, and the backporting cost is real.

Libraries and SDKs. Semantic versioning matters, consumers pin versions, and you may support several major versions. Release branches per major version.

Mobile applications. Staged rollout and app store review mean a version is “in flight” for days. A release branch covers that window.

Firmware and embedded. Long support lifetimes, no remote rollback, expensive defects. The most conservative case: release branches, long-lived maintenance branches, formal stabilisation.

This is the constraint most often ignored, and it caps how aggressive your branching can be.

Test maturitySafe to doNot safe
Comprehensive, fast, reliableTrunk-based, merge on green
Good but slowGitHub Flow, merge queueDirect-to-trunk
Patchy coverageFeature branches + manual QA on a release branchTrunk-based
FlakyFix the flakiness firstAnything that depends on CI as a gate

Flaky tests deserve emphasis. A suite that fails randomly teaches people to re-run until green, which removes the gate entirely while leaving it apparently in place. No branching model survives that.

Where segregation of duties, change approval or auditability is mandated, the constraint is usually not Git but the approval flow around it.

What tends to work: feature branches with mandatory review from someone other than the author, a release branch giving a defined stabilisation and approval window, signed commits and signed tags for attribution, and protection rules that make the process enforceable rather than procedural. See Signed Commits for what signatures do and do not establish.

Trunk-based development is not automatically excluded, but “merge equals deploy to production” often is. A common resolution is trunk-based development up to a staging environment, with a separate approved promotion to production.

Monorepo. Many teams share one main, so integration frequency matters more and long-lived branches hurt more. Per-path review rules and merge queues become valuable. Also where sparse checkout and partial clone start to matter.

Many small repositories. Each can choose its own model. Coordinating a change across several repositories is the hard part, and no branching model solves it.

Open source. Contributors are external and you do not control their pace. Fork-and-pull-request with maintainer review is effectively mandatory; maintainers typically run GitHub Flow internally plus release branches for supported versions.

Infrastructure repositories. Terraform, Kubernetes manifests, Ansible. Often small teams, high blast radius, and a review requirement driven by risk rather than volume. Feature branches with mandatory review, and an environment-promotion path rather than a release process.

ScenarioStart withWhy
Small SaaS team, deploys dailyGitHub FlowMinimal ceremony, one integration target
Large SaaS team, deploys continuouslyTrunk-based + merge queueIntegration cost dominates at scale
Solo projectDirect to main, branch when usefulNo reviewer, so no review workflow
Desktop or installed softwareRelease branches from main, maintenance branches per supported versionParallel versions are the defining constraint
Mobile appGitHub Flow + release branch per versionStore review creates an in-flight window
Library or SDKTrunk + release branch per major versionConsumers pin versions
Open source projectFork + pull request; release branches if versions are supportedNo control over contributor pace
Regulated productFeature branches + release branch + mandatory review and signingApproval and audit requirements
Infrastructure repoFeature branches, mandatory review, environment promotionHigh blast radius, low change volume
Team with weak or flaky testsFeature branches + a stabilisation stage — and fix the testsDo not remove a safety net you still need

Abstract criteria are easier to apply against concrete cases.

A seven-person team building a B2B web application

Section titled “A seven-person team building a B2B web application”

The answers. Deploys two or three times a week. One version in production. Rollback is a redeploy of the previous image, about four minutes. Test coverage is decent — integration tests cover the main flows, though some edge cases only surface in production. Nothing beyond CI must pass before release.

The reasoning. One version in production removes any need for maintenance branches. Rollback is fast, so the cost of a bad merge is bounded. Test coverage is “decent, not comprehensive”, which argues against committing directly to the trunk but not against merging on green with review.

The choice. GitHub Flow. Branch protection on main requiring CI plus one approval and an up-to-date branch. Tag main at each deployment so “what shipped Tuesday” has a name. No develop, no release branches.

What would change it. If deployment moved to several times a day and the team grew past fifteen, add a merge queue and start using feature flags — which is trunk-based development arrived at gradually.

A team shipping a desktop application quarterly

Section titled “A team shipping a desktop application quarterly”

The answers. Ships every three months. Three versions in active support, with security fixes backported to all three. No remote rollback — a bad release means shipping another one. Automated tests are reasonable but a two-week manual QA cycle catches things they do not. An external accessibility audit must pass before release.

The reasoning. Three supported versions is decisive: maintenance branches are mandatory, because there is nowhere else for 4.1 fixes to live once 4.3 is in development. A two-week QA cycle needs a frozen target that is not main, or development stops for a fortnight. No rollback raises the value of a stabilisation stage.

The choice. Trunk-based development on main for daily work, plus a release branch cut at feature freeze and a long-lived maintenance branch per supported version. Fixes land on main first and are cherry-picked back with -x, audited using git cherry -v.

What is deliberately not chosen. Full Git Flow. develop would add a second permanent integration target for no benefit — this team already has a release branch doing the stabilisation work, and one mainline is simpler.

The answers. Deploys whenever something is ready, several times a day. No users yet, so no versions to support and no rollback pressure. Tests are minimal. Nothing gates release.

The reasoning. Almost every constraint is absent. Process overhead is the dominant cost. Review by a second person is valuable but does not need to be a gate.

The choice. Commit to main. Branch when something is genuinely risky or when a second opinion is wanted before landing. No protection rules yet.

What would change it. The first paying customer. At that point rollback matters, tests matter, and the workflow needs to grow — starting with branch protection on main and tests worth gating on. The mistake here would be adopting heavyweight process now “to be ready”; the other mistake is still running this way with fifteen engineers and real customers.

Choosing by popularity. Git Flow’s diagram is widely reproduced, which is not evidence it fits your product. Trunk-based development is currently fashionable, which is not evidence your tests can support it.

Copying a company with different constraints. A large technology company’s monorepo practices are answers to problems of scale you probably do not have, supported by internal tooling you do not have either.

Optimising for the rare case. Designing your entire workflow around the hotfix you do twice a year adds daily cost for occasional benefit. Handle the rare case with a documented procedure, not a permanent branch.

Treating the model as the goal. The aim is shipping working software safely. If a strategy is technically correct and everyone routes around it, it has failed regardless of correctness.

Ignoring the merge method. Which branching model you use is often less consequential to daily experience than whether pull requests merge, squash or rebase. That is a separate decision, covered in Cluster 2, and worth making explicitly.

Never revisiting it. The right strategy for a five-person team is rarely right at forty. Re-examine after significant growth, a change in release model, or a substantial improvement in test coverage.

Choosing a branching model leaves one question open: when a branch integrates, what shape does it leave in main?

Methodmain historyBest when
Merge commitBranch structure preserved, explicit integration pointsBranch commits are individually meaningful; you want an audit trail of when integration happened
SquashOne commit per branchBranches are small, commits within them are working notes, and you want main to read as one change per unit of review
RebaseLinear, every branch commit preserved individuallyYou want linear history and the individual commits are worth keeping

This interacts with the branching model. Squash merging suits short-lived branches, where “one commit per pull request” is a fine granularity. It suits long-lived branches badly, because collapsing three weeks of work into one commit destroys genuinely useful history.

Squash Merging, Merge Commits and Rebase and Merge cover each in depth.

The models are components, not packages. The most common productive hybrids:

Trunk-based development plus release branches. Develop entirely on the trunk with short branches; cut a short-lived release branch when shipping a version. Gives continuous integration internally and versioned artefacts externally. Frequently what teams want when they think they need Git Flow.

GitHub Flow plus maintenance branches. Ordinary GitHub Flow for development, with a long-lived branch per supported version receiving backported fixes. Adds parallel version support without develop.

Feature branches plus feature flags. Keeps branches short without requiring full trunk-based discipline. Often the first step in an incremental migration.

Environment promotion instead of release branches. Rather than branching, promote the same artefact through environments — the commit that passed staging is the commit deployed to production. Common in infrastructure and container-based delivery, and it removes the merge-back problem entirely.

GitHub Flow plus develop. A second long-lived branch adds Git Flow’s integration cost without its release discipline. Pick one.

Trunk-based development with weak tests. Removes the stabilisation stage without replacing it. Failures move to production.

Git Flow with continuous deployment.main contains only released code” and “deploy on every merge” contradict each other. Teams in this position usually deploy from develop, at which point main is decorative.

Long-lived feature branches with any model. The one genuinely universal finding: branch lifetime drives integration cost regardless of what the branches are called.

A model nobody follows. A documented process that is routinely worked around is worse than a simpler one people actually use, because it makes the real process invisible.

Migrations fail when attempted all at once. A workable order:

  1. Measure first. Branch age, pull request size, review latency, CI duration and flake rate. These tell you what is actually wrong.
  2. Fix flaky tests. Everything else depends on trusting CI.
  3. Reduce change size. The highest-leverage change, and it is independent of any model.
  4. Speed up review. Branch lifetime cannot go below review latency.
  5. Add protection rules that encode what you have agreed.
  6. Then change the branch topology — usually the smallest change of the five.
  7. Re-measure. If branch age has not moved, the model was not the constraint.

Steps 2 to 4 deliver most of the benefit of any modern branching model, whatever you end up calling it.

Whatever you choose, write it down where people will see it — a CONTRIBUTING.md in the repository is the conventional place. A branching strategy that lives in senior engineers’ heads is rediscovered incorrectly by every new joiner.

What is worth recording is short:

  • Which branches are long-lived, and what each is for.
  • Where a change starts — branch from what, named how.
  • What must pass before integration — checks, approvals, an up-to-date branch.
  • How branches integrate — merge, squash or rebase, and who presses the button.
  • When branches are deleted.
  • How an urgent production fix works, since that is when people improvise.
  • How releases are cut and tagged, if that is a distinct event.

Encode as much of it as possible in protection rules rather than prose. A rule that requires an approving review is followed; a document that requests one is followed unevenly. Prose is for the parts tooling cannot express — mainly the “why”, which is what lets a future team judge whether the decision still holds.

A branching strategy is a negotiation between integration cost and isolation benefit.

Branching buys isolation: you can work without coordinating. It costs integration: someone must reconcile the divergence later. Every model is a different position on that trade.

Git Flow buys maximum isolation and pays maximum integration cost. Trunk-based development buys minimum isolation and pays almost nothing. GitHub Flow sits between.

The right position depends on how much isolation you genuinely need — which is mostly determined by how long a change takes to become safe, and that is a testing question more than a Git one.

  • No branching model is universally correct; fit is determined by release model, deployment frequency, test maturity, team size and regulatory context.
  • Parallel version support and stabilisation periods are what make release branches necessary.
  • Test quality caps how short your branches can safely be.
  • The models are components; hybrids are normal and usually better than adopting one wholesale.
  • Some combinations are actively contradictory, notably Git Flow with continuous deployment.
  • Branch lifetime drives integration cost under every model.
  • Migrating is mostly about tests, change size and review latency — branch topology changes last.

An audit rather than a lab. Do this against a repository you actually work in.

  1. Answer the five questions at the top of this lesson in writing.
  2. Measure branch age:
    Terminal window
    git for-each-ref --sort=-committerdate refs/heads/ \
    --format='%(committerdate:relative)%09%(refname:short)'
  3. Measure change size over the last twenty merges:
    Terminal window
    git log --oneline --shortstat --merges -20 main
  4. Count long-lived branches: how many are older than a week?
  5. Find your row in the recommendations table.
  6. Compare it with what your team actually does — not what the documentation says.
  7. Identify the single biggest gap, and whether it is a topology problem or a testing, size or review problem.

Step 7 is the point. In most audits the gap is not the branching model.

Branching decides how work diverges. Merging decides how it comes back together — and the choices there shape your history just as permanently.