Skip to content

🌲 Git Branching Models

Choosing the right branching strategy can make or break team collaboration and release cycles.

Git branching strategies overview
Different teams need different branching strategies

Note

Placeholder image above - would be ideal to have a visual showing the different branching models side by side

Why Branching Models Matter

When starting a new project or joining an existing team, one of the first questions that arises is: "How do we manage our git branches?" This seemingly simple question has profound implications for:

  • Release velocity - How quickly can you ship features to production?
  • Code stability - How do you prevent broken code from reaching users?
  • Team coordination - How do developers work in parallel without stepping on each other's toes?
  • Emergency response - How do you handle critical hotfixes whilst ongoing development continues?

The branching model you choose directly impacts all of these factors. A model that works brilliantly for a small team might become a bottleneck for a larger organisation, and vice versa. Let's explore the most common models and when each shines.

The Major Players

Git Flow

Vincent Driessen's Git Flow became hugely popular after its 2010 introduction. It's a comprehensive branching model designed for projects with scheduled releases.

Structure:

  • main - Production-ready code only
  • develop - Integration branch for features
  • feature/* - Individual feature branches
  • release/* - Release preparation branches
  • hotfix/* - Emergency fixes for production

Typical workflow:

# Start a new feature
git checkout -b feature/user-authentication develop

# Work on your feature
git add .
git commit -m "feat: implement JWT authentication"

# Finish the feature - merge back to develop
git checkout develop
git merge --no-ff feature/user-authentication
git branch -d feature/user-authentication

# Start a release
git checkout -b release/1.2.0 develop

# Finalise release (version bumps, changelog, etc.)
git commit -m "chore: bump version to 1.2.0"

# Merge to main and tag
git checkout main
git merge --no-ff release/1.2.0
git tag -a v1.2.0 -m "Release version 1.2.0"

# Merge back to develop
git checkout develop
git merge --no-ff release/1.2.0
git branch -d release/1.2.0

# Emergency hotfix
git checkout -b hotfix/1.2.1 main
# Fix the critical bug
git commit -m "fix: resolve null pointer in payment processing"

# Merge to both main and develop
git checkout main
git merge --no-ff hotfix/1.2.1
git tag -a v1.2.1

git checkout develop
git merge --no-ff hotfix/1.2.1
git branch -d hotfix/1.2.1
Git Flow branching diagram
Git Flow's structured approach to branching

Note

Placeholder for Git Flow diagram - ideally showing the parallel tracks of main, develop, feature, release, and hotfix branches

When to use Git Flow:

  • Scheduled release cycles (quarterly, monthly releases)
  • Multiple versions in production requiring maintenance
  • Large teams needing clear separation of concerns
  • Projects where release preparation involves significant work (version bumping, changelog generation, QA cycles)

When to avoid Git Flow:

  • Continuous deployment environments
  • Small teams moving fast
  • Projects with simple release processes
  • When the overhead of managing multiple long-lived branches outweighs the benefits

Trunk-Based Development

Trunk-based development is the polar opposite of Git Flow. Everyone commits to a single branch (the "trunk" or main) with minimal branching.

Structure:

  • main - The only long-lived branch
  • Short-lived feature branches (optional, lasting hours or days at most)

Typical workflow:

# Small change - commit directly to main
git checkout main
git pull
# Make your changes
git add .
git commit -m "feat: add logging to payment processor"
git push

# Larger change - short-lived branch
git checkout -b payment-retry-logic
# Work for a few hours
git add .
git commit -m "feat: implement exponential backoff for payment retries"

# Push and create PR immediately
git push -u origin payment-retry-logic
gh pr create --title "Add payment retry logic" --body "Implements exponential backoff"

# Merge within 24 hours
git checkout main
git pull
git merge payment-retry-logic
git push
git branch -d payment-retry-logic

Key practices for trunk-based development:

  • Feature flags - Hide incomplete features behind toggles
  • Continuous integration - Automated tests run on every commit
  • Small commits - Break work into mergeable increments
  • Quick reviews - PRs reviewed and merged within hours
# Example feature flag usage
def process_payment(amount: float, currency: str) -> bool:
    if feature_flags.is_enabled("new_retry_logic"):
        return process_with_retry(amount, currency)
    else:
        return legacy_payment_process(amount, currency)
Trunk-based development diagram
Trunk-based development's streamlined approach

Note

Placeholder for trunk-based diagram - showing frequent merges to main with optional short-lived branches

When to use trunk-based development:

  • Continuous deployment pipelines
  • Teams with strong testing culture
  • Mature CI/CD infrastructure
  • High-performing teams shipping multiple times per day
  • Microservices architectures

When to avoid trunk-based development:

  • Regulated industries requiring release approval processes
  • Teams without comprehensive automated testing
  • Projects with junior developers needing more review time
  • Products with scheduled release windows

GitHub Flow

GitHub Flow strikes a middle ground. It's simpler than Git Flow but more structured than pure trunk-based development.

Structure:

  • main - Production-ready code
  • Feature branches - One per feature or fix

Typical workflow:

# Create a descriptive branch
git checkout -b fix/memory-leak-in-cache

# Make commits with clear messages
git add cache.py
git commit -m "fix: prevent cache from growing unbounded"

git add tests/test_cache.py
git commit -m "test: add cache eviction tests"

# Push and open a pull request
git push -u origin fix/memory-leak-in-cache
gh pr create --title "Fix memory leak in cache" \
  --body "Implements LRU eviction strategy to prevent unbounded growth"

# After review and CI passes, merge to main
# This automatically deploys to production
git checkout main
git pull
git branch -d fix/memory-leak-in-cache

GitHub Flow principles:

  1. Anything in main is deployable
  2. Create descriptive branches for all work
  3. Push to remote regularly
  4. Open a PR when ready for feedback
  5. Merge only after review and passing CI
  6. Deploy immediately after merging
GitHub Flow diagram
GitHub Flow's pull request-centric workflow

Note

Placeholder for GitHub Flow diagram - showing feature branches merging to main via pull requests

When to use GitHub Flow:

  • Web applications with continuous deployment
  • Small to medium-sized teams
  • Projects where main is always deployable
  • When you want simplicity without sacrificing code review

When to avoid GitHub Flow:

  • Multiple production versions requiring support
  • Scheduled release cycles
  • When deployment and merging must be decoupled

GitLab Flow

GitLab Flow extends GitHub Flow with environment branches for projects that can't deploy every merge to production immediately.

Structure:

  • main - Latest integrated changes
  • pre-production - Staging environment
  • production - Production environment
  • Feature branches - For development

Typical workflow:

# Develop a feature
git checkout -b feature/api-rate-limiting main
git commit -m "feat: implement rate limiting middleware"

# Merge to main after review
git checkout main
git merge feature/api-rate-limiting
git push

# Deploy to staging
git checkout pre-production
git merge main
git push  # Triggers deployment to staging

# After QA approval, promote to production
git checkout production
git merge pre-production
git push  # Triggers production deployment

Environment-specific fixes:

# Fix discovered in staging
git checkout -b fix/staging-config-error pre-production
git commit -m "fix: correct database connection string"

# Merge to pre-production
git checkout pre-production
git merge fix/staging-config-error
git push

# Cherry-pick or merge back to main
git checkout main
git cherry-pick <commit-sha>
# or
git merge pre-production

When to use GitLab Flow:

  • Multiple deployment environments (dev, staging, production)
  • Approval gates between environments
  • When you need to test in staging before production
  • Continuous delivery (not deployment)

When to avoid GitLab Flow:

  • True continuous deployment scenarios
  • Simple projects without multiple environments
  • When the overhead of environment branches isn't justified

Choosing Your Model

The "best" branching model depends entirely on your context. Here's a decision framework:

Team Size and Experience

  • 1-3 developers: GitHub Flow or trunk-based development
  • 4-10 developers: GitHub Flow with strong CI/CD
  • 10-50 developers: Trunk-based development with feature flags, or Git Flow for scheduled releases
  • 50+ developers: Trunk-based development with robust feature flag infrastructure

Deployment Frequency

  • Multiple times per day: Trunk-based development
  • Daily: GitHub Flow
  • Weekly: GitHub Flow or GitLab Flow
  • Monthly or scheduled: Git Flow or GitLab Flow

CI/CD Maturity

High automation + excellent test coverage → Trunk-based development
Good automation + decent test coverage → GitHub Flow
Manual QA + approval processes → GitLab Flow or Git Flow
Limited automation → Git Flow (but improve your CI/CD!)

Release Complexity

  • One-click deploys: Trunk-based or GitHub Flow
  • Multi-environment promotion: GitLab Flow
  • Scheduled releases with prep work: Git Flow
  • Multiple supported versions: Git Flow

Common Pitfalls and Solutions

Long-Lived Feature Branches

Problem: Feature branches that live for weeks or months accumulate painful merge conflicts.

Solution:

# Regularly rebase or merge from main
git checkout feature/long-running-migration
git pull origin main --rebase

# Or use merge commits
git merge main

# Better yet, break the feature into smaller chunks
git checkout -b feature/migration-phase-1
# Merge within days, then start phase 2

Inconsistent Branch Naming

Problem: Branches named johns-stuff, fix, test123 make it impossible to understand what's being worked on.

Solution: Establish and enforce a naming convention:

# Good naming conventions
feature/user-authentication
fix/payment-processing-timeout
hotfix/security-patch-xss
refactor/extract-payment-service
docs/api-documentation

# Configure git aliases
git config --global alias.feature '!git checkout -b feature/$1'
git config --global alias.fix '!git checkout -b fix/$1'

# Use them
git feature user-authentication
git fix payment-timeout

The "Develop" Branch That Never Stabilises

Problem: When using Git Flow, develop becomes a dumping ground for half-finished features.

Solution:

  1. Feature flags to merge incomplete features safely
  2. Stricter merge criteria for develop
  3. Consider if Git Flow is the right model for your team
  4. Move to trunk-based development with better feature management

Hotfix Chaos

Problem: Emergency fixes get applied inconsistently across branches.

Git Flow approach:

# Create hotfix from main
git checkout -b hotfix/critical-security-fix main

# Apply fix
git commit -m "fix: sanitise user input in search endpoint"

# Merge to BOTH main and develop
git checkout main
git merge hotfix/critical-security-fix
git tag -a v1.2.1

git checkout develop
git merge hotfix/critical-security-fix

Trunk-based approach:

# Fix directly on main
git checkout main
git commit -m "fix: sanitise user input in search endpoint"
git push

# Deploy immediately
# No branch management needed!

Branching with Purpose

Regardless of which model you choose, the key is branching with purpose. Every branch should have:

  1. A clear goal - What is this branch for?
  2. A short lifespan - How long until it merges?
  3. A single owner - Who's responsible for it?
  4. An exit strategy - What are the merge criteria?

As I discussed in my post on breaking habits, creating branches without purpose leads to:

  • Abandoned branches cluttering the repository
  • Uncertainty about what's safe to delete
  • Merge conflicts from stale branches
  • Lost work from forgotten experiments
# Audit your branches regularly
git branch --merged main | grep -v "main" | xargs git branch -d
git branch --no-merged main

# Clean up remote branches
git fetch --prune
git remote prune origin

Real-World Examples

Apache Spark

The Apache Spark project uses a model similar to GitHub Flow:

  • Development happens on feature branches
  • All changes go through pull requests
  • The master branch (their terminology) represents the latest development state
  • Release branches are created from master for each release

Apache Arrow

Apache Arrow uses a more complex model with automated tooling. Their dev/merge_arrow_pr.py script handles:

  • Merging pull requests with proper commit message formatting
  • Cherry-picking commits to release branches
  • Updating JIRA tickets automatically
  • Ensuring commits have the right metadata

This demonstrates an important principle: complex branching models require automation. If you find yourself doing repetitive manual branch management, you're either using the wrong model or need better tooling.

Best Practices Across All Models

Regardless of which branching model you adopt:

1. Protect Your Main Branch

# Using GitHub CLI
gh api repos/:owner/:repo/branches/main/protection \
  --method PUT \
  --field required_status_checks[strict]=true \
  --field required_status_checks[contexts][]=continuous-integration \
  --field enforce_admins=true \
  --field required_pull_request_reviews[required_approving_review_count]=1

2. Automate Everything

# .github/workflows/ci.yml
name: CI
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Run tests
        run: pytest
      - name: Check coverage
        run: pytest --cov --cov-fail-under=80

3. Make Branch Policies Explicit

Create a CONTRIBUTING.md:

## Branching Strategy

We use GitHub Flow:

- `main` is always deployable
- Create feature branches from `main`
- Use descriptive names: `feature/add-payment-processing`
- Open a PR when ready for review
- Merge requires: 1 approval + passing CI
- Delete branches after merging

4. Review and Adapt

Your branching model should evolve with your team:

# Quarterly retrospective questions:
# - Are we shipping faster or slower than last quarter?
# - How often do we have merge conflicts?
# - What percentage of branches are merged within a week?
# - Are hotfixes causing problems?

# Collect metrics
git log --since="3 months ago" --oneline --graph | wc -l
git log --since="3 months ago" --merges | wc -l

Conclusion

There's no universally "correct" branching model. Git Flow works brilliantly for some teams and feels like bureaucratic overhead for others. Trunk-based development enables incredible velocity for mature teams but can be chaotic without the right practices in place.

The best branching model is the one that:

  • Matches your deployment frequency
  • Fits your team's size and maturity
  • Supports your release process
  • Minimises friction without sacrificing quality

Start simple—usually GitHub Flow—and add complexity only when you have a specific problem to solve. Most teams over-complicate their branching strategy when they'd be better served by improving their testing or deployment automation.

Whatever you choose, make it explicit, automate it where possible, and revisit it regularly. Your branching model should accelerate your team, not slow it down.

For more on git workflows and best practices, see my posts on:



  1. Vincent Driessen's original Git Flow post (2010) 

  2. Trunk-based Development comprehensive guide 

  3. GitHub Flow official guide 

  4. GitLab Flow documentation 

-->