🌲 Git Branching Models¶
Choosing the right branching strategy can make or break team collaboration and release cycles.
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 onlydevelop- Integration branch for featuresfeature/*- Individual feature branchesrelease/*- Release preparation brancheshotfix/*- 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
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)
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:
- Anything in
mainis deployable - Create descriptive branches for all work
- Push to remote regularly
- Open a PR when ready for feedback
- Merge only after review and passing CI
- Deploy immediately after merging
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 changespre-production- Staging environmentproduction- 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:
- Feature flags to merge incomplete features safely
- Stricter merge criteria for
develop - Consider if Git Flow is the right model for your team
- 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:
- A clear goal - What is this branch for?
- A short lifespan - How long until it merges?
- A single owner - Who's responsible for it?
- 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
masterbranch (their terminology) represents the latest development state - Release branches are created from
masterfor 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.
Related Reading¶
For more on git workflows and best practices, see my posts on:
- Breaking Habits - Developing better development habits, including branching with purpose
- Maintaining Code Quality - How CI/CD integrates with branching strategies
-
Trunk-based Development comprehensive guide ↩
-
GitHub Flow official guide ↩
-
GitLab Flow documentation ↩