Trunk-Based Development: The Branching Strategy Behind Google and Facebook
Explore Trunk-Based Development — the branching model used by Google, Meta, and Netflix. Learn about short-lived branches, feature flags, and continuous integration at scale.
Trunk-Based Development: The Branching Strategy Behind Google and Facebook
Trunk-Based Development is the branching strategy that powers the largest software organizations on Earth. Google, Meta, Netflix, and Amazon all use variations of this model. It is the simplest branching strategy in existence and the hardest to adopt.
The core idea: everyone commits to a single shared branch (the trunk) at least daily. Feature branches, if they exist, last hours — not days. Code that isn’t ready for users is hidden behind feature flags, not isolated in long-lived branches.
This post explains why the world’s most productive engineering teams use this approach, what it takes to make it work, and why most teams fail when they try.
When to Use / When Not to Use
Use Trunk-Based Development When
- Large engineering organizations — 50+ developers working on the same codebase
- Continuous deployment — Multiple deployments per day to production
- Strong CI/CD infrastructure — Automated testing that runs in minutes, not hours
- Feature flag platform — Infrastructure to toggle features on and off at runtime
- Experienced teams — Developers comfortable with small, incremental commits
Do Not Use Trunk-Based Development When
- Small teams without CI — Without automated testing, trunk breaks are frequent and costly
- Scheduled release cycles — If you ship monthly, the trunk model provides no structure
- Junior-heavy teams — Requires discipline around small commits and code review
- No feature flag infrastructure — Without flags, incomplete features block the trunk
- Regulated environments — Formal change management processes conflict with continuous commits
Core Concepts
Trunk-Based Development is built on four pillars:
| Pillar | Description |
|---|---|
| Single trunk | One shared branch (usually main) that is always releasable |
| Short-lived branches | Feature branches last hours, not days. Maximum 1-2 days. |
| Feature flags | Incomplete features are hidden behind runtime toggles, not branches |
| Continuous integration | Automated tests run on every commit, blocking broken code from trunk |
The model assumes that the cost of integrating code increases exponentially with branch age. A branch that’s one day old has trivial merge conflicts. A branch that’s two weeks old has conflicts in every file.
graph LR
A[main - Trunk] --> B[short feature 1]
A --> C[short feature 2]
A --> D[short feature 3]
B -->|hours| A
C -->|hours| A
D -->|hours| A
A --> E[Continuous Deploy]
Architecture and Flow Diagram
The complete Trunk-Based Development workflow with feature flags and CI gates:
graph TD
A[main - Always Releasable] -->|fork| B[feature branch]
B -->|commit| B
B -->|push + CI| C{CI Pass?}
C -->|No| D[Fix and retry]
D --> B
C -->|Yes| E[Code Review]
E -->|Approved| F[Squash merge to main]
F --> G[Deploy to Production]
G -->|Feature Flag OFF| H[Code deployed but hidden]
G -->|Feature Flag ON| I[Feature visible to users]
Step-by-Step Guide
1. Commit to Trunk Frequently
The golden rule: no developer should go more than one day without merging to trunk. In practice, top teams merge 3-10 times per day.
# Start with latest trunk
git checkout main
git pull origin main
# Create a short-lived branch
git checkout -b feat/add-search-filter
# Make small, focused changes
git add src/components/SearchFilter.tsx
git commit -m "feat: add search filter component skeleton"
# Push and open PR immediately
git push -u origin feat/add-search-filter
2. Use Feature Flags for Incomplete Work
Never let incomplete code block your merge. Hide it behind a flag:
// src/pages/SearchPage.tsx
import { useFeatureFlag } from '@/hooks/useFeatureFlag';
export function SearchPage() {
const showAdvancedFilters = useFeatureFlag('advanced-search-filters');
return (
<div>
<SearchInput />
{showAdvancedFilters && <AdvancedFilters />}
<SearchResults />
</div>
);
}
// Feature flag configuration
// Can be toggled without code deployment
const FEATURE_FLAGS = {
'advanced-search-filters': false, // Toggle to true when ready
'new-search-algorithm': true, // Already rolled out
};
3. Keep Branches Short-Lived
Break large features into small, mergeable increments:
# Instead of one massive branch for "search feature":
# Day 1: Search input component
git checkout -b feat/search-input
# ... implement, test, merge
# Day 1: Search results list
git checkout -b feat/search-results
# ... implement, test, merge
# Day 2: Search filters (behind flag)
git checkout -b feat/search-filters
# ... implement behind flag, test, merge
# Day 2: Search analytics
git checkout -b feat/search-analytics
# ... implement, test, merge
Each increment is independently mergeable and testable.
4. Code Review on Every Change
Code review is the primary quality gate in trunk-based development:
# Open PR with small, focused changes
gh pr create \
--title "feat: add search input component" \
--body "Part 1 of search feature. Adds the search input UI component.
Advanced filters and results come in follow-up PRs."
# Review should be fast — small PRs get reviewed quickly
# Target: review within 1 hour, merge within 4 hours
5. Automated Testing on Every Commit
Your CI pipeline must be fast enough to not block the flow:
# .github/workflows/trunk-ci.yml
name: Trunk CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
fast-checks:
# These run in parallel, target: < 5 minutes
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm test -- --testPathPattern=unit
integration-tests:
needs: fast-checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run test:integration
deploy:
needs: integration-tests
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./scripts/deploy.sh
Production Failure Scenarios + Mitigations
| Scenario | What Happens | Mitigation |
|---|---|---|
| Trunk break | A bad commit breaks the build for everyone | CI blocks the merge; if it slips through, revert immediately; blameless postmortem |
| Feature flag leak | A flagged feature is accidentally exposed to users | Automated tests verify flag states; staging environment with production flag config |
| Merge conflict storm | Multiple developers modify the same files simultaneously | Communicate in standup; use CODEOWNERS for sensitive areas; rebase frequently |
| Flag debt accumulation | Old feature flags clutter the codebase | Schedule flag cleanup sprints; set expiration dates on flags; automate flag detection |
| Partial feature deployment | Half a feature ships because the second PR isn’t ready | Design features to be incrementally valuable; use flags to hide incomplete pieces |
Trade-offs
| Aspect | Advantage | Disadvantage |
|---|---|---|
| Integration cost | Near-zero — branches merge daily | Requires discipline to keep branches short |
| Deployment frequency | Multiple times per day | Requires robust CI/CD and monitoring |
| Code quality | Continuous review catches issues early | Requires experienced developers |
| Feature management | Flags enable progressive rollout | Flag management adds complexity |
| Team coordination | Everyone works on the same version | Conflicts are resolved immediately, not deferred |
| Scalability | Works for thousands of developers | Requires significant infrastructure investment |
Implementation Snippets
Feature Flag SDK Pattern
// src/lib/featureFlags.ts
interface FeatureFlagConfig {
name: string;
enabled: boolean;
rolloutPercentage?: number;
expirationDate?: string;
}
class FeatureFlagManager {
private flags: Map<string, FeatureFlagConfig> = new Map();
async load(): Promise<void> {
const response = await fetch("/api/feature-flags");
const flags: FeatureFlagConfig[] = await response.json();
flags.forEach((f) => this.flags.set(f.name, f));
}
isEnabled(name: string, userId?: string): boolean {
const flag = this.flags.get(name);
if (!flag) return false;
if (!flag.enabled) return false;
if (flag.rolloutPercentage !== undefined) {
const hash = this.hashUserId(userId || "anonymous");
return hash % 100 < flag.rolloutPercentage;
}
return true;
}
// Check for expired flags
getExpiredFlags(): FeatureFlagConfig[] {
const now = new Date();
return Array.from(this.flags.values()).filter(
(f) => f.expirationDate && new Date(f.expirationDate) < now,
);
}
private hashUserId(id: string): number {
let hash = 0;
for (let i = 0; i < id.length; i++) {
hash = (hash << 5) - hash + id.charCodeAt(i);
hash |= 0;
}
return Math.abs(hash);
}
}
export const featureFlags = new FeatureFlagManager();
Pre-commit Hook — Enforce Small Commits
#!/bin/bash
# .git/hooks/pre-commit
# Warn if commit message doesn't follow convention
MSG_FILE=$1
MSG=$(cat "$MSG_FILE")
# Check for conventional commit prefix
if ! echo "$MSG" | grep -qE '^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?:'; then
echo "WARNING: Commit message should follow conventional commit format."
echo "Example: feat: add search filter component"
echo "Continue anyway? (y/n)"
read -r answer
if [ "$answer" != "y" ]; then
exit 1
fi
fi
Branch Age Monitor
#!/bin/bash
# scripts/check-branch-age.sh
# Alert when feature branches exceed 2 days
MAX_AGE_DAYS=2
THRESHOLD=$(date -d "$MAX_AGE_DAYS days ago" +%s 2>/dev/null || date -v-"${MAX_AGE_DAYS}d" +%s)
git branch -r --format='%(refname:short) %(committerdate:unix)' | \
grep 'origin/feature/' | \
while read -r branch timestamp; do
if [ "$timestamp" -lt "$THRESHOLD" ]; then
echo "WARNING: $branch is older than $MAX_AGE_DAYS days"
fi
done
Observability Checklist
- Logs: Log every feature flag evaluation in production for audit trails
- Metrics: Track branch age distribution, merge frequency per developer, and CI pipeline duration
- Traces: Trace each deployment back to the specific commits and PRs that triggered it
- Alerts: Alert when branch age exceeds 2 days, CI pipeline exceeds 15 minutes, or trunk break rate exceeds 1 per week
- Dashboards: Display trunk health metrics: build success rate, average merge time, flag count, and deployment frequency
Security and Compliance: Trunk-Based Development
- Feature flag governance: Treat production feature flags as deployment controls. Only authorized personnel should toggle flags in production.
- Flag access control: Implement role-based access for flag management. Developers can create flags in staging; only release managers toggle production flags.
- Flag audit trail: Log every flag change with timestamp, user identity, and business justification. This is critical for SOC 2 and HIPAA compliance.
- Flag expiration enforcement: CI should fail builds that contain flags past their expiration date. Stale flags are a security risk — they expand the attack surface.
- Code review enforcement: Use branch protection to require reviews before any merge to trunk. Automated CI pipelines serve as change approval records.
- Compliance mapping: Each trunk merge is a change event. Map merge commits to change tickets for audit purposes.
- Secret management: Never store flag configuration or toggle values in code. Use a secure flag management service with encryption at rest.
Common Pitfalls and Anti-Patterns
- The “Trunk” That Isn’t — Teams that call it trunk-based but allow week-long branches are just doing Git Flow with different names. Enforce branch age limits.
- Flag Sprawl — Hundreds of stale feature flags make the codebase unreadable. Set expiration dates and clean up regularly.
- Skipping Tests — Without fast, reliable tests, trunk breaks become daily events. Invest in CI infrastructure first.
- Big Bang Merges — Merging a week’s worth of work in one PR defeats the purpose. Break work into daily mergeable chunks.
- Flag Coupling — Features that depend on each other’s flags create complex toggle combinations. Design features to be independently flaggable.
- No Rollback Plan — When a trunk merge breaks production, you need instant rollback. Automated rollback on error rate spikes is essential.
- Ignoring Code Review — Fast merges without review lead to quality degradation. Small PRs enable fast review — don’t skip it.
Quick Recap Checklist
- Everyone commits to trunk at least once per day
- Feature branches exist for hours, not days
- Incomplete features are hidden behind feature flags
- CI pipeline runs on every commit and blocks broken code
- Code review is required for every merge to trunk
- Feature flags have expiration dates and are cleaned up regularly
- Trunk is always in a deployable state
- Failed deployments trigger automatic rollback
- Branch age is monitored and alerts fire for old branches
- Feature flag changes are logged and audited
Interview Q&A
Both use a single main branch, but the key difference is branch lifetime. GitHub Flow allows feature branches to exist for days or weeks while Trunk-Based Development enforces branches lasting hours to a maximum of 1-2 days.
Trunk-Based Development also relies more heavily on feature flags to manage incomplete work, whereas GitHub Flow may leave features in branches until they're complete. At scale, trunk-based requires more infrastructure investment but delivers higher integration velocity.
The immediate response is revert the commit — not fix forward. Getting the trunk green again is the highest priority. Then conduct a blameless postmortem to understand why the CI pipeline didn't catch the issue.
Prevention is better than cure: fast CI pipelines that run on every push, required code review, and small commits all reduce the probability of trunk breaks. Google reports that their trunk break rate is less than 1% of all commits.
Use a dedicated feature flag platform like LaunchDarkly, Split, or an internal solution. Key practices include:
- Expiration dates — Every flag has a planned removal date
- Automated cleanup — CI checks for flags past their expiration
- Rollout percentages — Gradually increase exposure from 1% to 100%
- Access control — Only authorized personnel can toggle production flags
- Audit logging — Every flag change is recorded with who, when, and why
At their scale, integration cost is the dominant bottleneck. With tens of thousands of developers, if everyone maintained long-lived branches, the merge conflicts would be unmanageable. Trunk-based development forces integration to happen continuously, keeping the cost per integration near zero.
They also have the infrastructure to support it: CI systems that can run millions of tests per day, feature flag platforms serving billions of evaluations, and monitoring systems that detect production issues within seconds.
Resources
- Trunk-Based Development official site — Comprehensive reference with branching patterns
- Google’s engineering practices — Software Engineering at Google book
- Feature Flags best practices — LaunchDarkly’s guide
- DORA State of DevOps Report — Annual report on software delivery performance
- Martin Fowler on Trunk-Based Development — Detailed analysis of branching patterns
Category
Tags
Related Posts
Choosing a Git Team Workflow: Decision Framework for Branching Strategies
Decision framework for selecting the right Git branching strategy based on team size, release cadence, project type, and organizational maturity. Compare Git Flow, GitHub Flow, and more.
Git Flow: The Original Branching Strategy Explained
Master the Git Flow branching model with master, develop, feature, release, and hotfix branches. Learn when to use it, common pitfalls, and production best practices.
Git Release Branching and Hotfixes: Managing Versions in Production
Master release branching and hotfix strategies in Git. Learn version branches, emergency fixes, backporting, and how to manage multiple production versions safely.