How to Choose a Git Branching Strategy
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.
Start with the questions that decide it
Section titled “Start with the questions that decide it”Most of the answer falls out of five questions. Answer these honestly before comparing models.
-
How often do you ship to users? Several times a day, weekly, monthly, or on a schedule set by someone else?
-
How many versions are in use at once? One (a web service), or several (installed software, libraries, mobile apps in staged rollout)?
-
Can you undo a bad change quickly? Revert and redeploy in minutes, or does fixing production take hours and coordination?
-
How good are your automated tests, really? Would you merge a change to
mainon a green build alone at 5pm on a Friday? -
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.
The models at a glance
Section titled “The models at a glance”| Feature branches | GitHub Flow | Git Flow | Trunk-based | |
|---|---|---|---|---|
| Long-lived branches | main | main | main + develop | trunk only |
| Typical branch lifetime | Days | Hours to days | Days to weeks | Hours |
| Release mechanism | Team’s choice | Merge to main | Release branch, tagged | Deploy trunk, or branch at release |
| Hotfix path | Ad hoc | Ordinary branch | Defined branch type | Ordinary branch, or revert |
| Parallel version support | Add release branches | Poor | Good | Add release branches |
| Requires strong CI | Helpful | Strongly | Helpful | Essential |
| Requires feature flags | Rarely | Often | Rarely | Effectively yes |
| Process overhead | Low | Low | High | Low process, high discipline |
| Integration cost | Grows with lifetime | Low | Highest | Lowest |
| Learning curve | Low | Low | Moderate | Moderate |
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.
Evaluating against your context
Section titled “Evaluating against your context”Team size
Section titled “Team size”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.
Deployment frequency
Section titled “Deployment frequency”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.
Release model
Section titled “Release model”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.
CI and test maturity
Section titled “CI and test maturity”This is the constraint most often ignored, and it caps how aggressive your branching can be.
| Test maturity | Safe to do | Not safe |
|---|---|---|
| Comprehensive, fast, reliable | Trunk-based, merge on green | — |
| Good but slow | GitHub Flow, merge queue | Direct-to-trunk |
| Patchy coverage | Feature branches + manual QA on a release branch | Trunk-based |
| Flaky | Fix the flakiness first | Anything 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.
Regulated environments
Section titled “Regulated environments”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.
Repository shape
Section titled “Repository shape”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.
Recommendations by scenario
Section titled “Recommendations by scenario”| Scenario | Start with | Why |
|---|---|---|
| Small SaaS team, deploys daily | GitHub Flow | Minimal ceremony, one integration target |
| Large SaaS team, deploys continuously | Trunk-based + merge queue | Integration cost dominates at scale |
| Solo project | Direct to main, branch when useful | No reviewer, so no review workflow |
| Desktop or installed software | Release branches from main, maintenance branches per supported version | Parallel versions are the defining constraint |
| Mobile app | GitHub Flow + release branch per version | Store review creates an in-flight window |
| Library or SDK | Trunk + release branch per major version | Consumers pin versions |
| Open source project | Fork + pull request; release branches if versions are supported | No control over contributor pace |
| Regulated product | Feature branches + release branch + mandatory review and signing | Approval and audit requirements |
| Infrastructure repo | Feature branches, mandatory review, environment promotion | High blast radius, low change volume |
| Team with weak or flaky tests | Feature branches + a stabilisation stage — and fix the tests | Do not remove a safety net you still need |
Three worked decisions
Section titled “Three worked decisions”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.
A two-person startup pre-launch
Section titled “A two-person startup pre-launch”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.
Where the decision usually goes wrong
Section titled “Where the decision usually goes wrong”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.
The merge method decision
Section titled “The merge method decision”Choosing a branching model leaves one question open: when a branch integrates, what shape does it leave
in main?
| Method | main history | Best when |
|---|---|---|
| Merge commit | Branch structure preserved, explicit integration points | Branch commits are individually meaningful; you want an audit trail of when integration happened |
| Squash | One commit per branch | Branches are small, commits within them are working notes, and you want main to read as one change per unit of review |
| Rebase | Linear, every branch commit preserved individually | You 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.
Combinations that work
Section titled “Combinations that work”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.
Combinations that do not work
Section titled “Combinations that do not work”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.
Changing strategy
Section titled “Changing strategy”Migrations fail when attempted all at once. A workable order:
- Measure first. Branch age, pull request size, review latency, CI duration and flake rate. These tell you what is actually wrong.
- Fix flaky tests. Everything else depends on trusting CI.
- Reduce change size. The highest-leverage change, and it is independent of any model.
- Speed up review. Branch lifetime cannot go below review latency.
- Add protection rules that encode what you have agreed.
- Then change the branch topology — usually the smallest change of the five.
- 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.
Documenting the decision
Section titled “Documenting the decision”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.
Mental Model
Section titled “Mental Model”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.
What You Learned
Section titled “What You Learned”- 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.
Try It Yourself
Section titled “Try It Yourself”An audit rather than a lab. Do this against a repository you actually work in.
- Answer the five questions at the top of this lesson in writing.
- Measure branch age:
Terminal window git for-each-ref --sort=-committerdate refs/heads/ \--format='%(committerdate:relative)%09%(refname:short)' - Measure change size over the last twenty merges:
Terminal window git log --oneline --shortstat --merges -20 main - Count long-lived branches: how many are older than a week?
- Find your row in the recommendations table.
- Compare it with what your team actually does — not what the documentation says.
- 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.
Next Cluster
Section titled “Next Cluster”Branching decides how work diverges. Merging decides how it comes back together — and the choices there shape your history just as permanently.