Guide

Git branching strategies explained

Git makes branching cheap, but cheap branches create expensive coordination problems if nobody agrees on the rules. Should main always be deployable? Do you cut release branches for every version? Can five developers merge to trunk ten times a day without breaking production? A branching strategy is the team contract that answers those questions. This guide compares the three models teams actually use — Git Flow, GitHub Flow, and trunk-based development — plus release trains and environment branches, merge vs rebase policy, pairing with CI/CD pipelines and blue-green deploys, a Harbor Commerce release workflow worked example, a strategy decision table, common pitfalls, and a production checklist. For Git object basics, start with our Git fundamentals guide.

What a branching strategy optimizes for

Every model trades off three tensions: isolation (can I work without stepping on teammates?), integration frequency (how often does my code meet everyone else’s?), and release predictability (can support ship hotfixes to production while development continues?). There is no universal winner — a mobile app with App Store review gates needs different rules than a SaaS API deployed hourly.

Good strategies also define branch lifetime. Short-lived feature branches (hours to days) reduce merge pain because the diff stays small and conflicts are rare. Long-lived branches (develop, release/2.4) accumulate drift: by the time you merge, half the codebase changed underneath you. The strategy you pick should make long-lived branches either unnecessary or explicitly managed.

Protected branches and policy as code

Naming conventions alone do not enforce behavior. Platforms like GitHub and GitLab let you mark main (and sometimes release/*) as protected: require pull request reviews, passing CI, signed commits, and block force-pushes. Encode your branching rules in repository settings and CI workflows so they survive team turnover. If the rule lives only in a wiki page, it will be violated the first time someone is in a hurry.

Git Flow: parallel development and release stabilization

Git Flow, popularized by Vincent Driessen, uses multiple long-lived branches with distinct roles:

  • main — always reflects production. Only merge from release/* or hotfix branches.
  • develop — integration branch for the next release. Feature branches merge here first.
  • feature/* — short-lived work branches cut from develop.
  • release/* — stabilization branch for a version: bug fixes only, no new features.
  • hotfix/* — emergency patches cut from main, merged back to both main and develop.

Git Flow shines when you ship versioned software on a schedule — desktop apps, embedded firmware, on-prem enterprise installs — and need a frozen window to QA a release while development continues on develop. The cost is ceremony: dual merges on every hotfix, release branch maintenance, and the risk that develop diverges from what production actually runs.

For continuous-deployment web services, Git Flow is usually overkill. Teams adopt it because the diagram looked authoritative in a blog post, then spend sprint time resolving merge-back conflicts between release/2.3 and develop. If you deploy to production multiple times per day, prefer a simpler model below.

GitHub Flow: one deployable main branch

GitHub Flow is intentionally minimal:

  1. main is always deployable.
  2. Create a descriptively named branch for each change.
  3. Open a pull request early for discussion and CI.
  4. Merge to main after review and green checks.
  5. Deploy main to production (often automatically).

There is no develop branch and no release branch unless you need one for a specific hotfix window. Feature flags (see our feature flags guide) decouple deployment from release: code lands on main dark, then flips on for a percentage of users after validation.

GitHub Flow assumes strong CI, code review culture, and the ability to roll back quickly — canary deploys or instant revert commits. It is the default for most SaaS teams because it minimizes branch inventory and keeps everyone integrating against the same tip.

Trunk-based development: integrate early, integrate often

Trunk-based development (TBD) pushes integration frequency further: developers commit to main (trunk) at least daily, often multiple times per day. Feature branches, when used at all, live for less than a day and are merged via pull request or direct push with review bots.

TBD requires investment upstream of Git: comprehensive automated tests, fast CI (<10 minutes), feature flags for incomplete work, and small incremental changes. The payoff is eliminating “integration week” before releases — pain surfaces within hours, not after a month-long feature branch merge.

Short-lived branches in TBD

Purist TBD allows only short-lived feature branches or branch by abstraction: ship a no-op code path to trunk, then fill it in across subsequent commits. Google and many high-velocity shops run variants of this. It is not casual — without flags and tests, trunk-based dev becomes trunk-based chaos.

Release trains and environment branches

Release trains schedule predictable integration windows: every two weeks, whatever merged to main since the last train rides to production together. Trains pair well with GitHub Flow or TBD when you need stakeholder predictability without maintaining a permanent develop branch.

Environment branches (staging, production) map Git refs to deployed environments. This pattern is common in GitOps setups where Argo CD watches a branch and reconciles cluster state. The anti-pattern is letting environment branches diverge for weeks — you are reinventing Git Flow with worse tooling. Prefer promoting immutable container tags through environments instead of long-lived environment-specific code forks.

Merge vs rebase policy

Teams should pick one default and document exceptions:

  • Merge commits preserve branch history and make reverts easy (git revert -m 1 on the merge). Main looks noisy.
  • Squash merge collapses a feature branch into one commit on main. Clean history, but you lose granular bisect points.
  • Rebase replays commits on top of latest main before merge. Linear history, but rewriting shared branches is dangerous.

A practical rule: rebase locally before opening a PR; squash or merge at the platform default for shared branches; never rebase commits others have pulled.

Worked example: Harbor Commerce checkout release

Harbor Commerce runs a payment checkout service on GitHub Flow with weekly release trains and feature flags. A developer starts feature/apple-pay-wallet from main, pushes on day one with a PR marked draft, and enables a apple_pay_checkout flag defaulting to off. CI runs unit tests, contract tests against the payment sandbox, and a preview deploy to a ephemeral environment.

Mid-week, another engineer merges a tax-calculation fix to main. The Apple Pay author rebases (or merges main into their branch) daily so the PR diff stays reviewable. On Thursday the PR passes review; squash-merge lands one commit on main. The flag remains off. Friday’s release train deploys main via blue-green cutover to staging, then production. Monday, SRE enables the flag for 5% of checkout sessions, watches error-rate dashboards, ramps to 100% by Wednesday.

A hotfix on Tuesday bypasses the train: hotfix/fix-3ds-timeout from main, fast-track review, merge, deploy within an hour, no release branch needed. If Harbor shipped boxed software instead, the same hotfix would cut from release/4.2 under Git Flow — illustrating how product shape drives strategy choice.

Strategy decision table

Model Best for Branch count Deploy frequency Key requirement
Git Flow Versioned releases, mobile, on-prem High (main, develop, release, feature) Weeks to months Release manager, merge discipline
GitHub Flow SaaS, web APIs, continuous delivery Low (main + short features) Daily to hourly CI, review, fast rollback
Trunk-based High-velocity platforms, large eng orgs Minimal (main + <1 day branches) Multiple per day Flags, fast tests, small diffs
Release train Stakeholder cadence without develop branch Low + calendar Fixed interval Flagged incomplete work, train conductor

Common pitfalls

  • Strategy mismatch — Git Flow on a service deployed 20×/day creates merge-back tax.
  • Long-lived feature branches — two-week branches without rebasing guarantee painful merges.
  • Unprotected main — direct pushes without CI skip the entire safety net.
  • Environment branch drift — staging code that never returns to main becomes a fork.
  • Rebase on shared branches — rewriting history others pulled corrupts their clones.
  • No rollback plan — branching strategy assumes you can revert or roll forward safely.
  • Flags as permanent storage — year-old flags mean trunk is not actually integrated.

Production checklist

  • Document the chosen model in the repo README; link to this guide for onboarding.
  • Protect main: require PR, passing CI, at least one reviewer.
  • Set a maximum feature branch lifetime (e.g. 3 days) and alert on violations.
  • Pick merge vs squash vs rebase defaults; enforce via platform settings.
  • Pair every merge with automated tests and preview deploy when possible.
  • Use feature flags for work-in-progress on trunk or GitHub Flow.
  • Define hotfix path: who approves, how fast CI runs, deploy authority.
  • Tag releases on main with semantic versions for audit trails.
  • Run quarterly retros on merge conflict frequency and CI duration.
  • Delete merged branches automatically to reduce clutter and mistaken checkouts.

Key takeaways

  • Branching strategy is a team contract — optimize for your release cadence, not a diagram.
  • Git Flow fits scheduled versioned releases; GitHub Flow fits continuous delivery.
  • Trunk-based development maximizes integration frequency but demands flags and fast CI.
  • Short-lived branches beat long-lived integration branches almost every time.
  • Protected main + CI + deploy rollback matter more than the label on your workflow.

Related reading