My first team project was a complete disaster. Five developers, one master branch, constant merge conflicts, broken builds that stayed broken for days. We were all competent developers, but we had no branching strategy. We needed structure, but which approach? After trying several strategies across different teams, here's what I learned actually works in practice.
The choice of branching strategy affects everything: how you develop features, how you deploy, how you handle bugs, and how your team collaborates. Get it wrong, and every day is painful. Get it right, and development flows smoothly.
Why Branching Strategy Matters
Without a clear branching strategy, chaos is inevitable. Let me paint a picture of what goes wrong:
Developers push directly to main, breaking it for everyone else. Features sit half-finished in the main branch for weeks. Production is broken, but the fix is mixed with unfinished work. You need to roll back a bug, but good luck figuring out which commits to revert. Someone force-pushes to main, and work gets lost. Code review happens after merge, if at all.
A good branching strategy prevents all of this by giving you:
- Clear separation: Stable production code stays separate from work-in-progress
- Deploy confidence: You can deploy at any time without fear
- Easy rollback: When things break, you know exactly what to revert
- Parallel development: Multiple developers work without stepping on toes
- Code review process: Changes are reviewed before hitting main
- Release management: You control what goes out and when
The right strategy depends on your team size, release cadence, and deployment model. Let's explore the main approaches.
Git Flow: The Traditional Approach
Git Flow was my introduction to structured branching, and it taught me why process matters. Created by Vincent Driessen in 2010, it's still widely used for good reason.
The Branch Structure
Git Flow uses five types of branches:
- main (or master): Production code only. Every commit here is a release.
- develop: Integration branch. Features merge here first.
- feature branches: One per feature. Branch from develop, merge back to develop.
- release branches: Preparation for production. Branch from develop when features are done.
- hotfix branches: Emergency fixes. Branch from main, merge to both main and develop.
The Workflow
Here's how development flows in Git Flow:
- Start a feature:
git checkout -b feature/user-auth develop - Work on the feature, commit regularly
- When done, merge back to develop:
git checkout develop && git merge feature/user-auth - When ready to release, create a release branch:
git checkout -b release/1.2.0 develop - Fix any last-minute issues in the release branch
- Merge to main:
git checkout main && git merge release/1.2.0 - Tag the release:
git tag -a v1.2.0 - Merge back to develop:
git checkout develop && git merge release/1.2.0
For hotfixes:
- Branch from main:
git checkout -b hotfix/critical-bug main - Fix the bug, commit
- Merge to main:
git checkout main && git merge hotfix/critical-bug - Tag:
git tag -a v1.2.1 - Merge to develop:
git checkout develop && git merge hotfix/critical-bug
When Git Flow Works
Git Flow excels in certain scenarios:
- Scheduled releases: If you release monthly or quarterly, Git Flow provides structure
- Multiple versions: Supporting v1.x and v2.x simultaneously? Git Flow handles this
- Strict separation: When production must never contain unfinished work
- Large teams: Clear process prevents chaos with many contributors
- Enterprise software: Projects with formal release processes and documentation
Git Flow's Downsides
But Git Flow has real costs:
- Complexity: Managing two main branches plus release branches is overhead
- Merge conflicts: Long-lived feature branches diverge, creating painful merges
- Deploy friction: You can't deploy directly from develop – need a release branch
- Double merging: Everything merges twice (to develop and main)
- Cognitive load: Developers need to remember which branch for what
For continuous deployment or small teams, Git Flow is overkill. That's where simpler strategies shine.
GitHub Flow: Simplicity Wins
After years with Git Flow, I discovered GitHub Flow and immediately understood why GitHub (the company) created it. It's dramatically simpler, and for most projects, that's exactly what you need.
The Rules
GitHub Flow has exactly one rule with three principles:
One rule: Main branch is always deployable
Three principles:
- Create a branch for each change (feature, bugfix, anything)
- Make a pull request for code review
- Deploy directly from main after merging
That's it. No develop branch, no release branches, no hotfix branches. Just main and feature branches.
The Workflow
# Start work
git checkout main
git pull
git checkout -b feature/add-payment-processing
# Work, commit, push
git add .
git commit -m "Add stripe integration"
git push -u origin feature/add-payment-processing
# Create pull request on GitHub
# Team reviews, approves
# Merge to main
# Deploy from main
Every commit to main:
- Goes through a pull request
- Has been code reviewed
- Passes all automated tests
- Is immediately deployable
If main breaks, fixing it becomes top priority for everyone. The branch protection ensures bad code can't sneak in.
Branch Protection Settings
GitHub Flow requires proper branch protection on main:
- Require pull requests (no direct pushes to main)
- Require at least one approval
- Require status checks (CI tests must pass)
- Require up-to-date branches before merging
- No force pushes allowed
These rules make GitHub Flow work. Without them, you're back to chaos.
When GitHub Flow Works
I use GitHub Flow for:
- Continuous deployment: Deploy multiple times per day
- Web applications: Single-version deployments (not software with installed versions)
- Small to medium teams: 2-20 developers
- Fast iteration: Projects where speed matters more than process
- Microservices: Individual services with independent deployment
GitHub Flow in Practice
On my current team, here's our actual workflow:
- Pick up a task from Jira
- Create a branch:
git checkout -b feature/PROJ-123-user-notifications - Work on the feature, commit regularly with meaningful messages
- Push and create a pull request when ready
- Request review from at least one teammate
- Address review comments
- When approved and CI passes, merge using "Squash and merge"
- Delete the feature branch
- CI automatically deploys to staging
- After testing in staging, promote to production
This flow lets us ship features in hours, not days or weeks.
Trunk-Based Development: The Extreme Approach
Trunk-based development takes simplicity even further, and it felt radical when I first encountered it: everyone commits directly to main (called trunk), or uses very short-lived branches (less than a day).
The Principles
- Commit to main multiple times per day
- Keep changes small and incremental
- Use feature flags for incomplete features
- Rely heavily on automated testing
- Fix broken builds immediately
Companies like Google and Facebook do this at massive scale. Google has a single monorepo with thousands of developers committing to trunk daily.
How It Actually Works
The key is feature flags (also called feature toggles). Deploy code to production, but hide unfinished features:
if (featureFlags.isEnabled('new-checkout-flow')) {
return ;
} else {
return ;
}
You can work on new-checkout-flow for weeks, deploying regularly, but users don't see it until you flip the flag. When it's ready, enable the flag and the feature goes live.
Requirements for Success
Trunk-based development only works with:
- Comprehensive testing: 80%+ code coverage, tests run on every commit
- Fast CI: Tests complete in under 10 minutes
- Feature flag system: Infrastructure for toggling features
- Small commits: Culture of breaking work into tiny increments
- Disciplined team: Everyone commits to keeping main stable
Without these, trunk-based development becomes chaos. But with them, it's incredibly fast.
When to Use It
Consider trunk-based development for:
- Mature teams with strong testing culture
- Projects with excellent automated test coverage
- Organizations that value deployment frequency
- Teams comfortable with feature flags
- Microservices where blast radius is small
Don't try this on a new team or project without tests. Build up to it.
What I Actually Recommend
After years of experience, here's my honest, practical advice based on your situation.
For Solo Developers
Just use main. Seriously. Create feature branches if you want to experiment, but don't overcomplicate things. Git is about safety and history, not process overhead.
For Small Teams (2-5 People)
GitHub Flow is perfect. It provides just enough structure without feeling bureaucratic:
- Branch for each feature/fix
- Pull request with one review
- Merge and deploy
Add branch protection to prevent accidents, and you're set.
For Medium Teams (5-15 People)
GitHub Flow with environment branches:
- Feature branches merge to main
- Main auto-deploys to staging
- When staging is stable, promote to production
This gives you a testing ground without Git Flow's complexity.
For Large Teams (15+ People)
Consider Git Flow if:
- You have scheduled releases (monthly/quarterly)
- You support multiple product versions
- You have formal QA and release processes
Otherwise, GitHub Flow still works with good discipline.
For Enterprise Software
Git Flow or a custom variant. You need the structure when:
- Customers run specific versions
- Releases require extensive documentation
- Multiple versions are supported simultaneously
- Compliance requires formal processes
Branch Naming Conventions
Whatever strategy you choose, use consistent naming. I follow this pattern:
feature/TICKET-123-short-description
bugfix/TICKET-456-fix-login-error
hotfix/critical-security-patch
refactor/improve-database-queries
docs/update-api-documentation
Benefits of this approach:
- Type is clear from prefix
- Ticket number links to issue tracker
- Description gives context
- Lowercase with hyphens (no spaces)
Making It Work in Practice
The best branching strategy is the one your team actually follows. Here's how to ensure success:
Document Your Strategy
Put it in your project's README or CONTRIBUTING.md:
# Branching Strategy
We use GitHub Flow:
1. Branch from main for each feature
2. Name branches: feature/description
3. Create PR when ready
4. Require one approval
5. Merge and deploy from main
Main branch is always deployable.
Automate Enforcement
Use GitHub branch protection, GitLab protected branches, or pre-commit hooks:
- Prevent direct pushes to main
- Require pull request reviews
- Require passing CI checks
- Require linear history (no merge commits)
Use CI/CD
Automate testing and deployment:
- Tests run on every push
- Main auto-deploys to staging
- Manual promotion to production
- Deployment status visible in PRs
Review Regularly
Every quarter, ask your team:
- Is our branching strategy working?
- Where do we have friction?
- Should we adjust our process?
Strategies should evolve as teams and products mature.
Common Pitfalls to Avoid
- Long-lived branches: Merge frequently (at least weekly). Long branches = painful merges
- Treating strategy as dogma: Rules serve the team, not vice versa. Adjust as needed
- No code review: Always review before merging, even in GitHub Flow
- Broken main: If main breaks, fix it immediately. Don't commit more features
- Over-complicating: Start simple, add complexity only when needed
The Bottom Line
Your branching strategy should enable your team, not burden it. The goal isn't perfect git history – it's shipping quality code reliably and sustainably.
Start with GitHub Flow for most projects. It's simple enough to follow consistently but structured enough to prevent chaos. Add complexity only when you genuinely need it, not because it seems "more professional."
Most importantly, whatever strategy you choose, document it, automate it, and actually follow it. A mediocre strategy followed consistently beats a perfect strategy ignored half the time.