Trunk-Based Development - A Deep Dive
Understanding trunk-based development, how it works, and why teams are adopting it for faster, more reliable software delivery.
If you've worked in software development for any length of time, you've probably encountered various branching strategies. Git Flow with its develop, release, and hotfix branches. Feature branches that live for weeks or months. Pull requests that sit waiting for review.
But there's a different approach that high-performing teams like Google, Facebook, and Netflix have been using for years: trunk-based development.
What You'll Learn:
- The core principles of trunk-based development
- How it compares to traditional branching strategies
- Using feature flags to enable continuous integration
- Real-world examples with code
- Making the transition to trunk-based development
What is Trunk-Based Development?
Trunk-based development (TBD) is a source-control branching model where developers collaborate on code in a single branch called "trunk" (or "main" or "master"), resisting any pressure to create long-lived feature branches.
The key principles are:
5 Core Principles:
- All developers commit to one branch - typically called
mainortrunk - Commits happen frequently - at least once per day, often multiple times
- Branches are short-lived - if they exist at all, they last hours or a day at most
- Code review happens quickly - within hours, not days
- Features are released using feature flags - not by branching
Visual Comparison: Different Branching Strategies
Let's visualize how different strategies look in practice.
Git Flow (Traditional Branching)
main ─────────●──────────────────●──────────────●─────
│ │ │
develop ─────●───┴──●───●───●──●───┴──●───●──●───┴─●─────
│ │ │ │
feature-A └──●───●───┘ │ │
│ │
feature-B └──●───●───┘
Timeline: ├─────────────── 2-3 weeks ─────────────────┤
Long-lived branches, complex merges, infrequent integration.
Feature Branches (Common Approach)
main ─────●─────────●──────────●────────●────────
┌──┘ ┌──┘ ┌──┘ ┌──┘
feature-1 └●──●──●──┘ │ │
┌──┘ │
feature-2 └●──●──●──●─┘
Timeline: ├─────── 5-10 days ────────┤
Moderate branch lifetimes, merge conflicts, delayed integration.
Trunk-Based Development
main ──●──●──●──●──●──●──●──●──●──●──●──●──●──●──
│ └──┘ │ └──┘ │ │ │ └──┘
dev1 dev2 dev1 dev3 dev2 dev1 dev3 dev2
Timeline: ├────── 1 day ─────┤
Continuous integration, short-lived (or no) branches, frequent commits.
How Does It Work in Practice?
The Basic Workflow
Let's walk through a typical day for a developer using trunk-based development:
Morning - 9 AM:
# Pull latest changes from trunk
git checkout main
git pull origin main
# Create a short-lived branch (optional)
git checkout -b add-user-validation
# Make changes
vim src/user/validator.js
Morning - 11 AM:
# Commit and push (after 2-3 hours of work)
git add src/user/validator.js
git commit -m "Add email validation for user signup"
git push origin add-user-validation
# Open PR (reviewed within 2-4 hours)
gh pr create --title "Add user email validation"
Afternoon - 2 PM:
# PR approved, merge to main
git checkout main
git pull origin main
# Branch is merged and deleted
Key point: The branch lived for only 4-5 hours. The changes are small, focused, and easy to review.
Feature Flags: The Secret Sauce
"But what if my feature takes weeks to build?" This is where feature flags come in.
Without Feature Flags
// Have to use long-lived branch
// File: checkout.js (on feature-branch for 2 weeks)
function processCheckout(cart, paymentMethod) {
// New payment system - not ready for production
const result = newPaymentGateway.charge(cart.total, paymentMethod)
if (result.success) {
sendConfirmationEmail(cart.user)
}
return result
}
With Feature Flags
// ✅ Can merge to main immediately
// File: checkout.js (on main branch)
function processCheckout(cart, paymentMethod) {
// Feature flag controls which payment system to use
if (featureFlags.isEnabled('new-payment-gateway')) {
// New system - gradually rolled out
const result = newPaymentGateway.charge(cart.total, paymentMethod)
if (result.success) {
sendConfirmationEmail(cart.user)
}
return result
}
// Old system - still works
return legacyPayment.process(cart, paymentMethod)
}
Now you can:
- Merge incomplete code safely to main
- Test in production with a small percentage of users
- Roll back instantly if there's an issue (just flip the flag)
- Continue iterating without branch conflicts
Feature Flag Lifecycle
Progressive Rollout Timeline:
Day 1-5: Flag OFF for everyone
├─ Code merged to main
└─ Deployed to production (dormant)
Day 6-7: Flag ON for 1% of users
├─ Monitor metrics
└─ Check for errors
Day 8-10: Flag ON for 10% of users
├─ Confidence building
└─ Performance validation
Day 11-12: Flag ON for 50% of users
└─ Full testing in production
Day 13: Flag ON for 100% of users
└─ Feature fully launched
Day 20: Remove flag from codebase
└─ Clean up dormant code path
Real Example: Adding a New Dashboard
Let's see how you'd build a new analytics dashboard using trunk-based development.
Day 1: Foundation
// File: src/dashboard/analytics.js
// Merged to main - behind feature flag
export function AnalyticsDashboard() {
if (!featureFlags.isEnabled('analytics-dashboard-v2')) {
return <LegacyDashboard />
}
return (
<div className="dashboard">
<h1>Analytics</h1>
{/* Basic structure only */}
</div>
)
}
git add src/dashboard/analytics.js
git commit -m "Add analytics dashboard skeleton (behind flag)"
git push origin main
Day 2: Add Charts
// File: src/dashboard/analytics.js
// Another commit to main
export function AnalyticsDashboard() {
if (!featureFlags.isEnabled('analytics-dashboard-v2')) {
return <LegacyDashboard />
}
return (
<div className="dashboard">
<h1>Analytics</h1>
<LineChart data={fetchUserMetrics()} /> {/* New addition */}
<BarChart data={fetchRevenueData()} /> {/* New addition */}
</div>
)
}
git add src/dashboard/analytics.js
git commit -m "Add charts to analytics dashboard"
git push origin main
Day 3: Enable for Testing
// File: config/feature-flags.js
export const featureFlags = {
'analytics-dashboard-v2': {
enabled: process.env.NODE_ENV === 'development' ||
isInternalUser(currentUser), // Enable for team
rolloutPercentage: 0
}
}
Now your team can test the dashboard in production, but customers still see the old version.
Day 5: Gradual Rollout
// File: config/feature-flags.js
export const featureFlags = {
'analytics-dashboard-v2': {
enabled: true,
rolloutPercentage: 10 // 10% of users
}
}
Day 10: Full Release
// File: config/feature-flags.js
export const featureFlags = {
'analytics-dashboard-v2': {
enabled: true,
rolloutPercentage: 100 // Everyone
}
}
Day 15: Clean Up
// File: src/dashboard/analytics.js
// Remove the flag and legacy code
export function AnalyticsDashboard() {
// No more flag check
return (
<div className="dashboard">
<h1>Analytics</h1>
<LineChart data={fetchUserMetrics()} />
<BarChart data={fetchRevenueData()} />
</div>
)
}
git add src/dashboard/analytics.js
git commit -m "Remove analytics dashboard feature flag"
git push origin main
The Benefits
1. Continuous Integration (Real CI)
Traditional feature branches don't actually integrate continuously. You integrate once when you merge. With trunk-based development, integration happens multiple times per day.
The difference is dramatic:
Traditional branches integrate once every 2 weeks. Trunk-based development integrates every few hours.
Feature Branch Model:
Integration Events: ──────────────────────────●──────────────────
├──── 2 weeks ───┤ (no integration) └─ merge
Trunk-Based Development:
Integration Events: ──●──●──●──●──●──●──●──●──●──●──●──●──●──●──
└─ every few hours
2. Reduced Merge Conflicts
Simple Math: Small, frequent changes = smaller, easier merges.
Example conflict scenario:
// Developer A (changes merged 2 hours ago)
function getUserProfile(userId) {
return db.users.findOne({ id: userId, active: true })
}
// Developer B (pulling latest before their change)
// They see the new signature immediately and adapt
function updateUserPreferences(userId, prefs) {
const user = getUserProfile(userId) // Uses latest API
return db.users.update({ id: user.id }, { preferences: prefs })
}
With long-lived branches, Developer B wouldn't see the change for weeks, leading to larger conflicts and painful merges.
3. Faster Feedback
Code in production = real feedback.
Traditional: Write → Branch (2 weeks) → Review → Merge → Deploy → Feedback
Timeline: ├───────────────── 3-4 weeks ─────────────────┤
Trunk-Based: Write → Merge → Deploy → Feedback
Timeline: ├──── 1-2 days ────┤
4. Lower Risk
Small changes are easier to:
| Activity | Small Change | Large Change |
|---|---|---|
| Review | 100 lines | 5,000 lines |
| Test | Focused scope | Complex interactions |
| Rollback | Flip a flag | Revert massive merge |
| Debug | Fresh in memory | Context lost |
The Challenges
1. Requires Discipline
Critical: You can't commit broken code to trunk.
This means:
| Requirement | Target |
|---|---|
| Test Coverage | High (80%+ for critical paths) |
| CI/CD Pipeline | Fast (< 10 minutes) |
| Quality Standards | Team agreement and enforcement |
2. Feature Flag Management
Beware: Flags can accumulate and create technical debt if not managed properly.
Bad - Flag sprawl:
// Don't let this happen
function checkout(cart) {
if (flags.newCheckout && flags.paymentV2 && !flags.legacyFlow) {
if (flags.betaUI || (flags.alphaUI && user.isAdmin)) {
// What's even happening here?
}
}
}
Good - Clean flag usage:
function checkout(cart) {
const processor = flags.isEnabled('payment-v2')
? new PaymentV2()
: new PaymentV1()
return processor.charge(cart)
}
// Remove flag within 2 weeks of 100% rollout
Best Practice: Remove flags within 2 weeks of reaching 100% rollout.
3. Cultural Shift
Teams used to long-lived branches need to adjust:
| Old Habit | New Habit |
|---|---|
| Build features for weeks | Break work into daily chunks |
| Review PRs in 2-3 days | Review within hours (same day) |
| Wait for "perfect" code | Trust teammates, iterate |
| Ship complete features | Focus on incremental delivery |
When Should You Use Trunk-Based Development?
Great For:
| Use Case | Why It Works |
|---|---|
| SaaS applications | Continuous deployment is valuable |
| Teams that deploy frequently | Daily or multiple times per day |
| Mature CI/CD pipelines | Automated testing and deployment |
| Small to medium teams | Easier coordination |
| Cloud-native applications | Built for continuous delivery |
Consider Alternatives For:
| Use Case | Challenge |
|---|---|
| Regulated industries | Might need release branches for auditing |
| Software with manual QA cycles | Can't deploy continuously |
| Teams with slow CI | If tests take hours, frequent commits are painful |
| Open source with external contributors | PRs from forks work better |
| Mobile apps | App store review process limits deployment frequency |
Making the Transition
If you want to adopt trunk-based development, here's a practical roadmap:
Phase 1: Speed Up Your Pipeline
Current State: Tests take 45 minutes
Goal: Tests take < 10 minutes
Split tests into parallel jobs:
# Run tests in parallel
npm run test:unit
npm run test:integration
npm run test:e2e
Phase 2: Implement Feature Flags
// Start simple
class FeatureFlags {
static isEnabled(flagName) {
const flags = {
'new-feature': process.env.NEW_FEATURE === 'true'
}
return flags[flagName] || false
}
}
## Conclusion
Trunk-based development isn't just a branching strategy—it's a mindset shift toward continuous integration and delivery.
:::tip
**The Payoff:**
- Faster feedback cycles
- Reduced risk
- Deploy multiple times per day
- Better team collaboration
:::
The key is to start small. You don't have to go from month-long feature branches to trunk-based development overnight. Gradually reduce branch lifetimes, invest in your CI/CD pipeline, introduce feature flags, and build team habits around frequent integration.
> The teams that have made this shift: Google, Facebook, Netflix, Etsy aren't doing it because it's trendy. They're doing it because it enables them to move faster, with higher quality, and with less risk. And that's a competitive advantage worth investing in.