Skip to content

Trunk-Based Development - A Deep Dive

date: October 25, 2025|read: 14 min|tags: [tutorial]

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:

  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:

ActivitySmall ChangeLarge Change
Review100 lines5,000 lines
TestFocused scopeComplex interactions
RollbackFlip a flagRevert massive merge
DebugFresh in memoryContext lost

The Challenges

1. Requires Discipline

Critical: You can't commit broken code to trunk.

This means:

RequirementTarget
Test CoverageHigh (80%+ for critical paths)
CI/CD PipelineFast (< 10 minutes)
Quality StandardsTeam 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 HabitNew Habit
Build features for weeksBreak work into daily chunks
Review PRs in 2-3 daysReview within hours (same day)
Wait for "perfect" codeTrust teammates, iterate
Ship complete featuresFocus on incremental delivery

When Should You Use Trunk-Based Development?

Great For:

Use CaseWhy It Works
SaaS applicationsContinuous deployment is valuable
Teams that deploy frequentlyDaily or multiple times per day
Mature CI/CD pipelinesAutomated testing and deployment
Small to medium teamsEasier coordination
Cloud-native applicationsBuilt for continuous delivery

Consider Alternatives For:

Use CaseChallenge
Regulated industriesMight need release branches for auditing
Software with manual QA cyclesCan't deploy continuously
Teams with slow CIIf tests take hours, frequent commits are painful
Open source with external contributorsPRs from forks work better
Mobile appsApp 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.