The Problem
Teams spend hours debating branching models and then adopt whichever one a tutorial showed them. The wrong choice is expensive: long-lived branches drift from main, merges turn into daylong conflict-resolution sessions, and "it works on my branch" becomes a weekly phrase. The branching model is not an aesthetic preference — it directly controls how often you can ship and how painful integration is.
The trade-off is simple: fewer, shorter-lived branches mean faster integration and continuous deployment; more, longer-lived branches mean more control but slower, more painful merges.
The Three Models
| Model | Branch structure | Integration cadence | Best for |
|---|---|---|---|
| <strong>Trunk-based</strong> | Everyone commits to <code class="inline-code">main</code> behind short-lived branches or feature flags | Many times a day | High-velocity teams, continuous deployment |
| <strong>GitHub Flow</strong> | <code class="inline-code">main</code> + short-lived feature branches merged via PR | Per PR, several times a day | Most teams, web apps, SaaS |
| <strong>Git Flow</strong> | <code class="inline-code">main</code>, <code class="inline-code">develop</code>, plus <code class="inline-code">feature</code>, <code class="inline-code">release</code>, <code class="inline-code">hotfix</code> branches | Per scheduled release | Versioned software, multiple supported releases |
Trunk-Based Development
Everyone integrates into main constantly — branches live hours, not days. Unfinished work hides behind feature flags rather than long-lived branches. This keeps integration pain near zero because nothing drifts far from trunk.
GitHub Flow
A lightweight middle ground: main is always deployable, and every change happens on a short-lived branch that opens a pull request. Once reviewed and green, it merges and deploys. This is the default for most teams and the one GitHub itself popularized.
Git Flow
A heavier model with permanent develop and main branches plus dedicated release and hotfix branches. It gives fine control over versioned releases but adds coordination overhead and slows integration. It fits software with scheduled releases and multiple supported versions — less so a continuously deployed web app.
The Short-Lived Branch Workflow
For GitHub Flow and trunk-based, the day-to-day loop is the same:
# start from an up-to-date main
git checkout main
git pull origin main
# create a short-lived, well-named branch
git checkout -b feature/add-password-reset
# work, commit, push
git add .
git commit -m "Add password reset endpoint"
git push -u origin feature/add-password-reset
# open a PR, get review + green checks, merge, delete the branch
The discipline that makes this work: keep the branch alive for hours or a day, not weeks. The longer it lives, the more main moves underneath it and the harder the merge becomes.
Pull Requests, Review, and Required Checks
A pull request is where branching meets CI/CD. Protect main so that nothing merges without passing checks and review.
In GitHub, branch protection rules on main typically require:
- At least one approving review before merge.
- Status checks to pass — your CI build and tests must be green.
- Branch up to date with
mainbefore merging. - No direct pushes to
main— changes arrive only through PRs.
# .github/workflows/pr-checks.yml — the required check behind the rule
name: PR Checks
on:
pull_request:
branches: [main]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- run: npm ci
- run: npm run lint
- run: npm test
Once this workflow is marked as a required status check, GitHub blocks the merge button until it passes. That is how a branching strategy becomes enforceable rather than a wiki page people ignore.
Release Tagging
Even with trunk-based or GitHub Flow, you still want a durable marker for what shipped. Tags give you that without a long-lived release branch.
git checkout main
git pull origin main
git tag -a v1.4.0 -m "Release 1.4.0"
git push origin v1.4.0
A pushed tag can itself trigger a release workflow:
on:
push:
tags:
- "v*" # fires on v1.4.0, v2.0.0, etc.
This keeps releases traceable while leaving your branch model simple.
How Branching Ties Into CI/CD
The branching model and the CI/CD pipeline are two halves of the same system:
- PR to
mainruns lint, build, and test as a required check — nothing merges red. - Merge to
maintriggers deploy to staging (and often production) automatically. - Tag push cuts a versioned release.
Short-lived branches make this flow safe because main stays continuously deployable. Long-lived branches fight it: by the time a two-week branch merges, the pipeline is validating code against a main that has changed hundreds of times.
Recommendation
For most teams deploying continuously — web apps, APIs, SaaS — use GitHub Flow, and move toward trunk-based as your test coverage and feature-flagging mature. Both keep integration cheap and main shippable. Reserve Git Flow for software with real scheduled releases and multiple supported versions, where its extra branches earn their keep.
Common Mistakes to Avoid
- Long-lived feature branches. The longer a branch lives, the worse the merge. Keep them to hours or a day and integrate often.
- No branch protection on
main. Without required reviews and checks, your strategy is a suggestion. Enforce it with protection rules. - Adopting Git Flow by default. Its
develop/release/hotfixoverhead rarely pays off for a continuously deployed app. Start simpler. - Treating
mainas unstable.mainshould always be deployable. If it is routinely broken, your required checks are too weak. - Skipping release tags. Even with simple branching, tags give you a traceable record of exactly what shipped and when.
FAQ
Which branching strategy should a new team pick?
Start with GitHub Flow: main plus short-lived feature branches merged through reviewed pull requests. It is simple, keeps main deployable, and maps cleanly onto GitHub's protection rules and CI — you can always graduate to trunk-based later.
What makes trunk-based development different from GitHub Flow?
Trunk-based pushes integration even further: developers merge into main many times a day and hide unfinished work behind feature flags rather than branches. GitHub Flow still uses short-lived branches per change, which is a gentler on-ramp to the same continuous-integration mindset.
How do protected branches enforce a strategy?
Branch protection rules on main can require approving reviews, passing status checks, and an up-to-date branch before any merge, while blocking direct pushes. That turns your intended workflow into something GitHub enforces automatically instead of relying on everyone remembering the rules.
Do I still need release branches with GitHub Flow?
Usually not. Pushing an annotated tag like v1.4.0 from main gives you a durable release marker and can trigger a release workflow, without the overhead of maintaining a separate long-lived release branch.