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.

14 min read
tutorial

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:

  1. All developers commit to one branch - typically called main or trunk
  2. Commits happen frequently - at least once per day, often multiple times
  3. Branches are short-lived - if they exist at all, they last hours or a day at most
  4. Code review happens quickly - within hours, not days
  5. 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:

  1. Merge incomplete code safely to main
  2. Test in production with a small percentage of users
  3. Roll back instantly if there's an issue (just flip the flag)
  4. 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.