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 fromrelease/*or hotfix branches.develop— integration branch for the next release. Feature branches merge here first.feature/*— short-lived work branches cut fromdevelop.release/*— stabilization branch for a version: bug fixes only, no new features.hotfix/*— emergency patches cut frommain, merged back to bothmainanddevelop.
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:
mainis always deployable.- Create a descriptively named branch for each change.
- Open a pull request early for discussion and CI.
- Merge to
mainafter review and green checks. - Deploy
mainto 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 1on 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
mainbefore 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
mainwith 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
- Git fundamentals explained — commits, branches, merge vs rebase mechanics
- CI/CD pipelines explained — automate test and deploy on every merge
- Blue-green and canary deployments explained — safe production cutovers
- Feature flags explained — decouple merge from user-visible release