Introduction — Why Branching Strategies Matter
If you can only change one thing about your engineering organisation's delivery speed, change the branching strategy. Research from the DORA team (Accelerate, 2018) consistently shows that deployment frequency correlates strongly with team performance — and your branching model is the single biggest constraint on how often you can deploy.
A branching strategy is your team's agreement about when code splits from the main line, how long it lives in isolation, and what conditions must be met before it merges back. Get this wrong and you'll face merge conflicts measured in days, releases delayed by weeks, and hotfixes that break other features.
The Relationship Between Branching and Deployment Frequency
Consider this spectrum: at one extreme, a team using full GitFlow with release branches might deploy every 2-4 weeks. At the other extreme, a team using trunk-based development deploys dozens of times per day. Both can be correct — for different contexts.
flowchart LR
A[GitFlow] --> B[GitLab Flow]
B --> C[GitHub Flow]
C --> D[Trunk-Based Dev]
A --- E["Deploy: 2-4 weeks"]
B --- F["Deploy: 1-2 weeks"]
C --- G["Deploy: Daily"]
D --- H["Deploy: Multiple/day"]
style A fill:#132440,color:#fff
style B fill:#16476A,color:#fff
style C fill:#3B9797,color:#fff
style D fill:#BF092F,color:#fff
The critical question isn't "which strategy is best?" but rather "what does your business need?" A medical device company with FDA compliance needs GitFlow's rigour. A startup shipping a SaaS product needs trunk-based development's speed.
GitFlow — The Classic Enterprise Model
Vincent Driessen published "A successful Git branching model" in 2010, and it became the default for enterprise teams. GitFlow defines five branch types with strict rules about how code flows between them.
The Five Branch Types
| Branch | Lifetime | Purpose | Created From | Merges Into |
|---|---|---|---|---|
main |
Permanent | Production-ready code | — | — |
develop |
Permanent | Integration branch | main | release, main |
feature/* |
Temporary | New feature work | develop | develop |
release/* |
Temporary | Release preparation | develop | main + develop |
hotfix/* |
Temporary | Production bug fixes | main | main + develop |
gitGraph
commit id: "init"
branch develop
commit id: "dev-1"
branch feature/login
commit id: "feat-1"
commit id: "feat-2"
checkout develop
merge feature/login id: "merge-feat"
branch release/1.0
commit id: "bump-version"
commit id: "fix-typo"
checkout main
merge release/1.0 id: "v1.0" tag: "v1.0.0"
checkout develop
merge release/1.0 id: "back-merge"
checkout main
branch hotfix/1.0.1
commit id: "critical-fix"
checkout main
merge hotfix/1.0.1 id: "v1.0.1" tag: "v1.0.1"
checkout develop
merge hotfix/1.0.1 id: "hotfix-merge"
When GitFlow Works
GitFlow excels in specific contexts:
- Multiple supported versions — If you maintain v2.x and v3.x simultaneously (desktop software, mobile SDKs, embedded firmware), GitFlow's release branches are essential
- Scheduled releases — If your organisation releases on fixed cadences (quarterly, monthly), the release branch gives a stabilisation period
- Regulatory compliance — When each release requires sign-off, audit trails, and traceability, GitFlow's explicit branching makes compliance documentation straightforward
- Large teams with siloed features — When 50+ developers work on independent features that must be batched into releases
Common Pitfalls
The most common GitFlow failure is long-lived feature branches. When a feature branch lives for 3+ weeks, it diverges so far from develop that merging becomes a multi-day project. The solution — if you must use GitFlow — is to keep feature branches under 5 days and rebase frequently.
# GitFlow: Creating a feature branch
git checkout develop
git pull origin develop
git checkout -b feature/user-authentication
# Work on the feature...
git add .
git commit -m "feat: add JWT token validation"
# Keep up-to-date with develop (do this daily)
git fetch origin
git rebase origin/develop
# When ready, merge back to develop
git checkout develop
git merge --no-ff feature/user-authentication
git push origin develop
git branch -d feature/user-authentication
# GitFlow: Creating a release branch
git checkout develop
git checkout -b release/2.1.0
# Bump version numbers
echo "2.1.0" > VERSION
git add VERSION
git commit -m "chore: bump version to 2.1.0"
# Fix release-critical bugs (no new features!)
git commit -m "fix: correct pagination off-by-one"
# Merge to main AND develop when ready
git checkout main
git merge --no-ff release/2.1.0
git tag -a v2.1.0 -m "Release 2.1.0"
git push origin main --tags
git checkout develop
git merge --no-ff release/2.1.0
git push origin develop
git branch -d release/2.1.0
GitHub Flow — The Simplified Model
GitHub Flow strips branching down to its essence: there is main (always deployable) and feature branches (short-lived work). That's it. No develop branch. No release branches. No hotfix branches.
gitGraph
commit id: "init"
branch feature/search
commit id: "search-1"
commit id: "search-2"
checkout main
merge feature/search id: "PR #42"
commit id: "deploy-1" tag: "deployed"
branch feature/dark-mode
commit id: "dark-1"
checkout main
branch fix/typo
commit id: "typo-fix"
checkout main
merge fix/typo id: "PR #43"
commit id: "deploy-2" tag: "deployed"
merge feature/dark-mode id: "PR #44"
commit id: "deploy-3" tag: "deployed"
The GitHub Flow Process
- Create a branch from main with a descriptive name
- Add commits — small, atomic, well-described
- Open a Pull Request — start discussion, request review
- Review and discuss — CI runs tests automatically
- Merge — squash or merge commit into main
- Deploy — main is always deployable; deploy immediately
# GitHub Flow: The complete workflow
# Step 1: Create branch from main
git checkout main
git pull origin main
git checkout -b feature/add-search-api
# Step 2: Make commits
git add src/search.ts
git commit -m "feat: implement search endpoint with Elasticsearch"
git add tests/search.test.ts
git commit -m "test: add search endpoint integration tests"
# Step 3: Push and open PR
git push origin feature/add-search-api
# Open PR via GitHub CLI:
gh pr create --title "feat: add search API" \
--body "Implements full-text search using Elasticsearch" \
--reviewer @team-lead
# Step 4-5: After review approval, merge via GitHub UI or CLI
gh pr merge --squash
# Step 6: main is deployed automatically via CI/CD pipeline
GitHub Flow works best for:
- Web applications and SaaS — where there's only one version in production
- Small to medium teams (2-20 developers)
- Continuous deployment — deploying multiple times per day
- Products without multiple supported versions
GitHub's Own Deployment Practice
GitHub itself uses GitHub Flow (naturally). Their engineering team deploys to production dozens of times per day. Every merged PR triggers a deployment pipeline that includes automated tests, canary deployment, and progressive rollout. The average time from merge to production is under 15 minutes.
Key enabler: comprehensive automated testing (>50,000 tests) combined with feature flags for incomplete work. Engineers can merge code that isn't customer-visible yet.
GitLab Flow — The Middle Ground
GitLab Flow sits between GitFlow's complexity and GitHub Flow's simplicity. It introduces environment branches that map to deployment environments, solving a problem GitHub Flow ignores: what if you can't deploy every merge immediately?
Environment Branches
In GitLab Flow, code flows downstream through environment branches:
# GitLab Flow: Environment branch promotion
# Developers merge to main (like GitHub Flow)
git checkout main
git merge --no-ff feature/new-dashboard
# Promote main → staging (manual or automated trigger)
git checkout staging
git merge main
git push origin staging
# → triggers staging deployment
# After staging validation, promote to production
git checkout production
git merge staging
git push origin production
# → triggers production deployment
Comparison with Other Flows
| Aspect | GitFlow | GitHub Flow | GitLab Flow |
|---|---|---|---|
| Long-lived branches | main + develop | main only | main + environment branches |
| Release branches | Yes (mandatory) | No | Optional |
| Deploy trigger | Merge to main | Merge to main | Merge to environment branch |
| Staging environment | Ad-hoc | Feature branch deploys | Dedicated branch |
| Best for | Versioned releases | Continuous deployment | Regulated environments |
Trunk-Based Development
Trunk-Based Development (TBD) is the most extreme simplification: everyone commits directly to main (the "trunk"), or uses feature branches that live for less than one day. There is no develop branch, no release branches, no integration branches. Main is always deployable because it's always being deployed.
Feature Flags for Incomplete Work
The apparent contradiction — "how do you merge unfinished features into main?" — is solved by feature flags. Code exists in main but is invisible to users until the flag is enabled:
// Feature flag pattern for trunk-based development
// All code merges to main immediately, but is gated behind flags
const featureFlags = {
newCheckoutFlow: process.env.FF_NEW_CHECKOUT === 'true',
darkMode: process.env.FF_DARK_MODE === 'true',
aiRecommendations: process.env.FF_AI_RECS === 'true'
};
function renderCheckout(cart) {
if (featureFlags.newCheckoutFlow) {
// New checkout — still in development
return renderNewCheckout(cart);
}
// Existing checkout — shown to all users
return renderLegacyCheckout(cart);
}
// Feature flags can be:
// 1. Boolean (on/off)
// 2. Percentage-based (10% of users)
// 3. User-targeted (internal employees only)
// 4. Environment-specific (staging: on, production: off)
console.log("Feature flags loaded:", featureFlags);
# Trunk-Based Development: Typical developer workflow
# Start of day — pull latest trunk
git checkout main
git pull origin main
# Create a very short-lived branch (optional, max 1 day)
git checkout -b wz/add-search-index
# Make focused changes
git add src/search/indexer.ts
git commit -m "feat: add search indexer behind FF_SEARCH flag"
# Push and create PR (reviewed within hours, not days)
git push origin wz/add-search-index
gh pr create --title "feat: search indexer (flagged)" --reviewer @pair-partner
# PR is reviewed immediately (often within 30 minutes)
# After approval, merge and delete branch
gh pr merge --squash
git checkout main
git pull origin main
git branch -d wz/add-search-index
# The CI/CD pipeline deploys to production automatically
# Feature is invisible to users because FF_SEARCH=false in production
Trunk-Based Development at Scale
The world's largest engineering organisations use trunk-based development:
- Google — 25,000+ engineers commit to a single monorepo trunk. Over 80,000 commits per day. Automated testing gates every change.
- Meta — Engineers commit to trunk with "landcastle" (a pre-merge test system that validates changes before they land)
- Netflix — Trunk-based with feature flags. Their system handles thousands of deployments per day across microservices
- Spotify — Moved from GitFlow to trunk-based development as they scaled. Deployment frequency increased from weekly to many-times-daily
Trunk-Based Development and Elite Performance
The DORA research programme (spanning 7 years and 36,000+ respondents) found that elite-performing teams are 2x more likely to practice trunk-based development than low performers. They also found that teams with branches living longer than one day had significantly lower deployment frequency and higher change failure rates.
The correlation is clear: short-lived branches (or no branches at all) correlate with higher throughput and better stability — contradicting the intuition that more branching equals more safety.
Workflow Comparison Matrix
Use this table to choose the right workflow for your team:
| Dimension | GitFlow | GitHub Flow | GitLab Flow | Trunk-Based |
|---|---|---|---|---|
| Deploy frequency | 2-4 weeks | Daily | 1-2 weeks | Multiple/day |
| Team size | 10-100+ | 2-20 | 5-50 | Any (with maturity) |
| Complexity | High | Low | Medium | Low (ops: High) |
| Release model | Versioned | Continuous | Staged | Continuous |
| CI requirement | Moderate | High | High | Very high |
| Feature flags needed | No | Optional | Optional | Essential |
| Multiple versions | Yes | No | Yes | No |
| Merge conflicts | Frequent (large) | Rare (small) | Occasional | Minimal |
Monorepos vs Polyrepos
Orthogonal to branching strategy is repository structure. A monorepo contains all of an organisation's code in a single repository. A polyrepo (or multi-repo) approach uses one repository per service, library, or team.
Monorepo Advantages
- Atomic changes — A single commit can update an API, its client library, and the documentation simultaneously
- Code sharing — Shared libraries live alongside consuming code, eliminating versioning overhead
- Unified CI — One pipeline, one set of tools, one configuration
- Visibility — Every developer can see, search, and learn from all code
- Dependency management — One version of each dependency across the entire organisation
Polyrepo Advantages
- Team autonomy — Teams own their repo, their CI, their release cadence
- Access control — Trivial to restrict access per repository
- Tooling simplicity — Standard Git works without custom infrastructure
- Smaller clone sizes — Developers only clone what they need
- Independent deployability — Each service has its own lifecycle
Monorepo Tooling
| Tool | Language | Key Feature | Used By |
|---|---|---|---|
| Bazel | Any | Hermetic builds, remote caching | Google, Stripe |
| Nx | JS/TS | Affected detection, computation caching | Many OSS projects |
| Turborepo | JS/TS | Incremental builds, remote caching | Vercel, many startups |
| Pants | Python/Go/Java | Fine-grained invalidation | Toolchain |
| Lerna | JS/TS | Package publishing | Babel, Jest (legacy) |
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"packages/*",
"apps/*"
],
"scripts": {
"build": "turbo run build",
"test": "turbo run test",
"lint": "turbo run lint",
"dev": "turbo run dev --parallel"
},
"devDependencies": {
"turbo": "^2.0.0",
"typescript": "^5.4.0"
}
}
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": []
},
"lint": {
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
}
}
}
Branch Protection & Policies
Regardless of which branching strategy you choose, protecting your main branch is non-negotiable for professional teams. Branch protection rules enforce quality gates before code reaches production.
Essential Protection Rules
- Required reviews — At least 1-2 approved reviews before merge
- Status checks — CI must pass (build, test, lint) before merge is allowed
- Signed commits — Verify author identity with GPG keys
- Linear history — Require rebase or squash merge (no merge commits)
- Up-to-date branches — Branch must be current with main before merging
- No force push — Prevent history rewriting on protected branches
# GitHub branch protection via API (or configure in Settings > Branches)
# .github/settings.yml (using probot/settings app)
branches:
- name: main
protection:
required_pull_request_reviews:
required_approving_review_count: 2
dismiss_stale_reviews: true
require_code_owner_reviews: true
required_status_checks:
strict: true
contexts:
- "ci/build"
- "ci/test"
- "ci/lint"
- "security/snyk"
enforce_admins: true
required_linear_history: true
restrictions:
users: []
teams: ["release-managers"]
# Setting up commit signing (required for signed commits policy)
# Generate GPG key
gpg --full-generate-key
# List keys and get the key ID
gpg --list-secret-keys --keyid-format=long
# Output: sec rsa4096/ABC123DEF456 2024-01-01
# Configure Git to use the key
git config --global user.signingkey ABC123DEF456
git config --global commit.gpgsign true
# Export public key for GitHub
gpg --armor --export ABC123DEF456
# Paste output into GitHub > Settings > SSH and GPG keys
# Verify signed commits
git log --show-signature -1
Case Studies
Google's Monorepo and Trunk-Based Development
Google stores virtually all of its code — billions of lines across thousands of projects — in a single monorepo called "google3". Over 25,000 engineers commit to the same trunk daily, producing 80,000+ commits per day.
How it works:
- Custom VCS called Piper (Perforce-derived) handles the scale Git cannot
- TAP (Test Automation Platform) runs affected tests for every change
- Changes go through mandatory code review before submission
- Feature flags gate all incomplete work
- Automated code health tools refactor code globally
Result: Despite enormous scale, Google ships hundreds of production changes per hour with low failure rates.
Atlassian's GitFlow to Trunk-Based Migration
Atlassian (makers of Jira and Bitbucket) initially promoted and used GitFlow extensively. As their products moved to cloud-first SaaS delivery, they found GitFlow's overhead unsustainable:
- Merge conflicts consumed 15-20% of developer time
- Release branches delayed features by 2-3 weeks
- Hotfix branches created divergence nightmares
Migration: They moved to trunk-based development with feature flags over 6 months. Deployment frequency increased from bi-weekly to daily. Their documentation (atlassian.com/git) now recommends trunk-based for SaaS products.
Exercises
Conclusion & Next Steps
Your branching strategy is not a religious choice — it's an engineering decision driven by your deployment model, team size, compliance needs, and product lifecycle. GitFlow serves versioned software with regulatory requirements. GitHub Flow serves SaaS teams deploying continuously. Trunk-based development serves organisations that invest in engineering maturity to achieve maximum delivery speed.
The trend across the industry is clear: shorter-lived branches, faster merges, and more automation. If your branches live longer than 2 days, you're accumulating integration risk. If they live longer than a week, you're almost certainly creating unnecessary merge conflicts.
Next in the Series
In Part 12: Build Systems & Dependency Management, we'll explore how code transforms into deployable artifacts — npm, Maven, Gradle, pip, lock files, reproducible builds, and the dependency management practices that prevent "works on my machine" failures.