Skip to content

Branching Strategies

What You'll Learn

  • How trunk-based development, GitHub flow, GitFlow, and release branches work
  • How to choose a strategy based on how you deliver software
  • How merge commits, squash merges, and rebase merges affect history
  • Why feature flags make short-lived branches possible, and how GitOps repositories are laid out

Why the Strategy Matters

A branching strategy decides how long changes live apart before they're integrated. The longer branches live, the bigger and riskier every merge becomes. Research on software delivery performance, including the DORA reports, consistently associates short-lived branches and frequent integration with higher delivery performance.

Trunk-Based Development

Everyone integrates into one main branch (the trunk) at least daily. Branches, if used, live for hours to a day or two.

gitGraph
  commit id: "a"
  branch fix-timeout
  commit id: "b"
  checkout main
  merge fix-timeout
  branch add-metric
  commit id: "c"
  checkout main
  merge add-metric
  commit id: "d" tag: "v1.8.0"
  branch retry-flag
  commit id: "e"
  checkout main
  merge retry-flag
  • main is always releasable, protected by CI.
  • Unfinished features are merged behind feature flags, not held back on branches.
  • Releases are tags on main, or deploys of every commit.

Best for: continuous delivery, web services, teams with solid automated tests.

GitHub Flow

A lightweight version of the same idea:

  1. Branch from main.
  2. Commit and push.
  3. Open a pull request; CI runs; reviewers approve.
  4. Merge to main.
  5. Deploy main.

It's trunk-based development with pull requests as the integration step, and it's the default for most teams on GitHub and GitLab.

GitFlow

A structured model with long-lived branches:

gitGraph
  commit id: "1.0" tag: "v1.0"
  branch develop
  commit id: "d1"
  branch feature/search
  commit id: "f1"
  commit id: "f2"
  checkout develop
  merge feature/search
  branch release/1.1
  commit id: "rc fix"
  checkout main
  merge release/1.1 tag: "v1.1"
  checkout develop
  merge release/1.1
  checkout main
  branch hotfix/1.1.1
  commit id: "hotfix"
  checkout main
  merge hotfix/1.1.1 tag: "v1.1.1"
  checkout develop
  merge hotfix/1.1.1
Branch Lifetime Purpose
main Permanent Released code only
develop Permanent Integration of finished features
feature/* Days to weeks One feature
release/* Until release Stabilization and version bumps
hotfix/* Hours Urgent fixes to production, merged to both main and develop

GitFlow fits software shipped in versioned releases on a schedule — desktop apps, on-premises products, libraries. For continuously deployed services, it adds overhead and delays integration; even its original author has since recommended simpler workflows for web applications delivered continuously.

Release Branches

When you must support several versions at once (for example 2.x and 3.x of a library or product), keep trunk-based development on main and cut release branches for maintenance:

git switch -c release/2.4 v2.4.0
# Fix on main first, then bring the fix back to the release branch
git switch release/2.4
git cherry-pick -x 9f3c2d1          # -x records the original commit in the message
git tag -a v2.4.1 -m "Release 2.4.1"

Fixing on main first and cherry-picking back ensures the fix isn't lost in the next major version.

Choosing a Strategy

Your situation Choose
Web services deployed many times a day with good automated tests Trunk-based development or GitHub flow
A small team starting out GitHub flow
Versioned releases customers install, several supported versions Trunk-based with release branches
Scheduled releases with a formal stabilization phase GitFlow, knowing it slows integration
Regulated environments needing approvals per environment GitHub flow plus deployment environments and approvals in CI/CD, not per-environment branches

Merge Methods

Pull requests can be merged three ways:

Method Result on main Keep when
Merge commit All branch commits plus a merge commit Individual commits matter and are well curated
Squash merge One commit per pull request Branch commits are noisy; you want one revertible unit per change
Rebase merge Branch commits replayed linearly, no merge commit Commits are curated and you want linear history

Many teams allow squash only: one pull request becomes one commit, git revert undoes a whole change, and git log on main reads like a changelog. The pull request title becomes the commit message, so write good titles.

Feature Flags

Short-lived branches need a way to merge incomplete work safely:

if flags.is_enabled("order-retries", user=current_user):
    submit_with_retries(order)
else:
    submit(order)
  • Code is merged and deployed dark, then released by flipping the flag — for internal users first, then a percentage, then everyone.
  • A bad feature is turned off in seconds without a rollback.
  • Remove flags after launch. Stale flags become permanent branches in your code.

Real incident: Knight Capital (2012) and a reused flag

On 1 August 2012 Knight Capital deployed new trading code that reused an old feature flag. One of its eight servers didn't get the new code, and on that server the flag switched on long-dead logic that bought high and sold low. Knight lost about $460 million in 45 minutes (SEC order). Two flag rules come from it: never reuse a flag's name for new behavior, and delete flags and the dead code behind them once a launch is done. The deployment side of the story is in Idempotency.

Branches and Environments in GitOps

A common mistake is one Git branch per environment (dev, staging, prod), promoted by merging between them. Branches drift, merges carry unrelated changes, and it's hard to see what differs between environments.

Prefer one branch, one folder per environment:

platform-config/
├── base/
│   ├── deployment.yaml
│   └── kustomization.yaml
└── overlays/
    ├── dev/
    │   └── kustomization.yaml       # image tag abc1234, 1 replica
    ├── staging/
    │   └── kustomization.yaml       # image tag abc1234, 2 replicas
    └── prod/
        └── kustomization.yaml       # image tag 9e8d7c6, 6 replicas

Promotion is a pull request that changes the image tag in the next environment's folder — reviewable, auditable, and trivially revertible. See GitOps with ArgoCD and Flux.

What Most Engineers Miss

  • Trunk-based development needs a merge queue at scale. With many pull requests merging an hour, each one is tested against a main that has already moved. A merge queue (GitHub's, GitLab's merge trains, or Bors-style tools) tests each change on top of the ones ahead of it, so main stays green without forcing everyone to rebase.
  • Branch by abstraction beats a long branch. For a large change, such as swapping a database client, add an interface, move callers to it in small merges, build the new implementation behind it, then switch. Every step merges to main the same day.
  • A release branch is not GitFlow. Cutting release/2.4 from main and only cherry-picking fixes into it works fine with trunk-based development. GitFlow's cost is the long-lived develop branch, not release branches.
  • Feature flags are configuration, and configuration fails. Treat flag changes like deploys: audited, gradual, and reversible, with an owner and an expiry date for every flag.

Common Mistakes

  • Adopting GitFlow for a continuously deployed web service and wondering why integration is painful.
  • Feature branches that live for weeks because there's no feature-flag mechanism.
  • One long-lived branch per environment, merged forward, with environments silently diverging.
  • Fixing a bug on a release branch and forgetting to fix main.
  • Allowing every merge method, so main's history is an unreadable mix.

Check Yourself

Integrating daily

A team practicing trunk-based development has a feature that will take three weeks. How do they keep merging daily?

  • Merge it to main only when it's finished
  • Merge small steps behind a feature flag (or branch by abstraction), with the flag off in production
  • Keep a long-lived feature branch and rebase it daily

The code is integrated and tested continuously; the flag decides when users see it.

Interview Questions

  • Compare trunk-based development and GitFlow. When would you choose each?
  • How do feature flags enable trunk-based development?
  • What are the trade-offs between squash merges and merge commits?
  • How would you structure branches for a product that supports versions 2.x and 3.x?
  • Why is a branch per environment an anti-pattern for GitOps, and what do you use instead?

Next

Continue to Undo and Recovery.