Git Branching Strategies: GitFlow, GitHub Flow & Trunk-Based Development
Why Branching Strategies Matter
Every software team uses Git, but how you organize branches determines whether your workflow is a smooth highway or a merge-conflict minefield. A branching strategy defines how developers create, name, merge, and delete branches. The right strategy depends on your team size, release cadence, and deployment pipeline. The wrong one leads to integration nightmares, stale branches, and deployment fear.
This guide compares the three most popular strategies -- GitFlow, GitHub Flow, and Trunk-Based Development -- with concrete examples, branch diagrams, and decision criteria so you can pick the one that fits your team.
Strategy Comparison at a Glance
| Feature | GitFlow | GitHub Flow | Trunk-Based |
|---|---|---|---|
| Long-lived branches | main + develop | main only | main only |
| Feature branches | Yes (days-weeks) | Yes (hours-days) | Optional (hours) |
| Release branches | Yes | No | No |
| Hotfix branches | Yes | No (use feature) | No (commit to trunk) |
| Merge strategy | Merge commits | Squash merge / PR | Rebase / direct push |
| Release cadence | Scheduled | Continuous | Continuous |
| Feature flags needed | No | Sometimes | Yes (critical) |
| Best for | Versioned software, mobile apps | Web apps, small teams | Large teams, CD pipelines |
GitFlow: The Classic Enterprise Workflow
GitFlow, introduced by Vincent Driessen in 2010, uses two long-lived branches (main and develop) plus three types of supporting branches: feature, release, and hotfix. It is designed for projects with scheduled releases and multiple versions in production.
Branch Structure
main ──●──────────────────●──────●──── (production releases)
\ / /
release/1.0 \──●──●──────● / (stabilization)
\ \ / /
develop ──●──●──●──●──●──●──●──── (integration branch)
\ / \ /
feature/login ●──● \ /
●──●
feature/auth
Branch naming:
feature/user-login
feature/JIRA-123-payment
release/1.2.0
hotfix/critical-auth-fixGitFlow Workflow Commands
# Start a new feature
git checkout develop
git checkout -b feature/user-dashboard
# Work on the feature... commit changes
git add .
git commit -m "Add dashboard layout with chart widgets"
# Finish feature -- merge back into develop
git checkout develop
git merge --no-ff feature/user-dashboard
git branch -d feature/user-dashboard
# Create a release branch when develop is ready
git checkout develop
git checkout -b release/1.2.0
# Fix bugs on the release branch, update version
git commit -m "Bump version to 1.2.0"
# Finish release -- merge into main AND develop
git checkout main
git merge --no-ff release/1.2.0
git tag -a v1.2.0 -m "Release 1.2.0"
git checkout develop
git merge --no-ff release/1.2.0
git branch -d release/1.2.0
# Emergency hotfix from main
git checkout main
git checkout -b hotfix/fix-payment-crash
# Fix the bug...
git checkout main
git merge --no-ff hotfix/fix-payment-crash
git tag -a v1.2.1 -m "Hotfix 1.2.1"
git checkout develop
git merge --no-ff hotfix/fix-payment-crash
git branch -d hotfix/fix-payment-crashWhen to Use GitFlow
- Mobile apps with app store review cycles (iOS, Android)
- Desktop software with versioned releases (v1.0, v2.0)
- Enterprise projects with QA/staging environments per release
- Teams supporting multiple major versions simultaneously
When to Avoid GitFlow
- Web applications deployed continuously (too much ceremony)
- Small teams under 5 developers (unnecessary complexity)
- Projects without version numbers or scheduled releases
GitHub Flow: Simple and Continuous
GitHub Flow, popularized by GitHub itself, strips branching down to the essentials: one main branch, short-lived feature branches, pull requests, and continuous deployment. There is no develop branch, no release branches, and no hotfix branches. Every merge to main is deployed to production.
The Complete Workflow
# 1. Create a feature branch from main
git checkout main
git pull origin main
git checkout -b add-search-filter
# 2. Make commits with clear messages
git add src/components/SearchFilter.tsx
git commit -m "Add search filter component with debounce"
git add src/hooks/useSearch.ts
git commit -m "Add useSearch hook with fuzzy matching"
# 3. Push and open a Pull Request
git push -u origin add-search-filter
# Open PR on GitHub -- triggers CI/CD checks
# 4. Code review + automated tests pass
# 5. Merge PR (squash or merge commit)
# GitHub merges and optionally deploys to production
# 6. Delete the branch
git checkout main
git pull origin main
git branch -d add-search-filterBest Practices for GitHub Flow
- Keep branches short-lived -- merge within 1-3 days. Longer branches accumulate merge conflicts and integration risk.
- Write descriptive PR titles -- your PR title becomes the merge commit message. Make it meaningful for git log.
- Require CI checks before merge -- set up branch protection rules to prevent merging broken code.
- Use squash merge for clean history -- squash merge combines all PR commits into one clean commit on main.
- Deploy after every merge -- if you are not deploying after every merge, GitHub Flow is not the right fit.
Trunk-Based Development: The Speed-First Approach
Trunk-Based Development (TBD) takes simplicity to its logical extreme: all developers commit directly to the main branch (the "trunk") or use very short-lived feature branches that last hours, not days. Google, Facebook, Netflix, and most high-performing engineering teams use TBD.
How It Works
# Option A: Commit directly to main
git checkout main
git pull --rebase origin main
# Make small, focused changes
git add src/utils/format.ts
git commit -m "Add currency formatter with locale support"
git push origin main
# Option B: Very short-lived branch (merged same day)
git checkout main
git pull --rebase origin main
git checkout -b fix-date-parser
# Fix in 2-3 hours max
git add .
git commit -m "Fix date parser timezone offset bug"
git checkout main
git merge fix-date-parser
git push origin main
git branch -d fix-date-parser
# Feature flags for incomplete work
// config/features.ts
export const FEATURES = {
NEW_DASHBOARD: process.env.FEATURE_DASHBOARD === 'true',
BETA_SEARCH: process.env.FEATURE_SEARCH === 'true',
}
// In your component
import { FEATURES } from '@/config/features'
function App() {
return FEATURES.NEW_DASHBOARD
? <NewDashboard />
: <LegacyDashboard />
}Prerequisites for Trunk-Based Development
- Comprehensive automated tests -- every commit goes to production, so your test suite must catch regressions
- Feature flags -- you need a way to deploy incomplete features without exposing them to users
- Fast CI/CD pipeline -- builds must complete in minutes, not hours, to keep the trunk green
- Small, incremental commits -- each commit should be independently deployable and safe
- Team discipline -- no one commits code that breaks the build
Branch Naming Conventions
Consistent branch naming makes your repository navigable and your CI/CD rules predictable. Here are the most common conventions across all three strategies.
| Type | Pattern | Examples |
|---|---|---|
| Feature | feature/description | feature/user-auth, feature/JIRA-456-search |
| Bug fix | fix/description | fix/login-redirect, fix/null-pointer |
| Release | release/version | release/1.2.0, release/2026-q1 |
| Hotfix | hotfix/description | hotfix/crash-on-login, hotfix/security-patch |
| Chore | chore/description | chore/update-deps, chore/ci-config |
Use lowercase, hyphens as separators, and include a ticket number if your team uses issue tracking. Validate branch names with a regex pattern like (feature|fix|hotfix|release|chore)/[a-z0-9-]+ in a pre-push hook.
Merge vs Rebase: Which to Use
The merge-vs-rebase debate is one of the most common Git discussions. The answer depends on your branching strategy and what you optimize for: history clarity or traceability.
| Aspect | Merge | Rebase |
|---|---|---|
| History | Preserves full branch history | Creates linear history |
| Merge commits | Creates merge commit | No merge commit |
| Conflict resolution | Once per merge | Per rebased commit |
| Safe for shared branches | Yes | No (rewrites history) |
| Best used for | Integrating feature into main | Updating feature from main |
# Golden rule: rebase to update, merge to integrate
# Update your feature branch with latest main
git checkout feature/search
git rebase main
# Resolve conflicts per commit if any
# Integrate feature into main (via PR or manual merge)
git checkout main
git merge --no-ff feature/search
# Creates a merge commit -- clear integration point
# Squash merge for cleaner history (GitHub PR option)
git checkout main
git merge --squash feature/search
git commit -m "Add search with fuzzy matching and filters"Choosing the Right Strategy for Your Team
Use this decision framework to pick the strategy that matches your team's reality, not its aspirations.
| If your team... | Use | Reason |
|---|---|---|
| Ships versioned releases (v1, v2) | GitFlow | Release branches for stabilization |
| Deploys web apps continuously | GitHub Flow | Simple, PR-based, CI/CD friendly |
| Has 2-5 developers | GitHub Flow | Minimal overhead, easy to learn |
| Has 50+ developers on one repo | Trunk-Based | Avoids long-lived branch conflicts |
| Deploys 10+ times per day | Trunk-Based | Branches add unnecessary latency |
| Supports multiple prod versions | GitFlow | Parallel release/hotfix branches |
Many teams start with GitHub Flow and migrate to Trunk-Based Development as their CI/CD pipeline matures. GitFlow is less common in modern web development but remains the standard for mobile and desktop software with app store release cycles.
Automating Branch Policies
Branch protection rules enforce your strategy automatically. Here is an example GitHub Actions workflow that validates branch names and requires checks before merging.
# .github/workflows/branch-policy.yml
name: Branch Policy
on:
pull_request:
types: [opened, synchronize]
jobs:
validate-branch:
runs-on: ubuntu-latest
steps:
- name: Check branch name
run: |
BRANCH=${{ github.head_ref }}
if [[ ! "$BRANCH" =~ ^(feature|fix|hotfix|chore)/ ]]; then
echo "Invalid branch name: $BRANCH"
echo "Must start with feature/, fix/, hotfix/, or chore/"
exit 1
fi
- name: Check PR size
uses: actions/github-script@v7
with:
script: |
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
});
const changes = files.reduce((sum, f) =>
sum + f.additions + f.deletions, 0);
if (changes > 500) {
core.warning(
`Large PR: ${changes} lines changed. Consider splitting.`
);
}You can validate branch names, commit messages, and other patterns with a Regex Tester before hardcoding them into CI/CD configs. For scheduled automation like stale branch cleanup, cron expressions handle the timing.
Frequently Asked Questions
What is the best Git branching strategy for small teams?
Should I use git merge or git rebase for feature branches?
git rebase main) to keep a linear history. Use merge (via pull request) when integrating the feature branch back into main, as this creates a merge commit that clearly marks when the feature was integrated. Never rebase branches that other developers have pulled -- this rewrites shared history and causes conflicts.What is Trunk-Based Development and when should I use it?
Validate Your Git Patterns
Test branch naming conventions, commit message formats, and CI/CD patterns with our free Regex Tester. Build and validate patterns directly in your browser.
Open Regex Tester