Blog
/

Trunk-Based Development: How Feature Flags Enable True Continuous Integration

Alex Rivera
··
EngineeringBest Practices
Trunk-Based Development: How Feature Flags Enable True Continuous Integration

If you've ever lost a day to resolving merge conflicts or waited weeks for a feature branch to be "ready" to merge, you understand the pain of long-lived branches. There's a better way: trunk-based development powered by feature flags.

The Branch Problem

Many teams follow Git Flow or similar branching models:

  • Feature branches that live for days or weeks
  • Develop branches that accumulate changes
  • Release branches for stabilization
  • Hotfix branches for production issues

This creates problems:

Merge conflicts: The longer branches live, the more conflicts accumulate Integration issues: Code works in isolation but breaks when merged Code review fatigue: Reviewing 50-file diffs is exhausting Deployment delays: Can't deploy until feature is "done" Context switching: Developers juggle multiple branches

A tree with a broken branch alongside new growth, illustrating the fragility of long-lived feature branches.

Trunk-Based Development

In trunk-based development, everyone commits to main (trunk) at least daily. There are no long-lived feature branches.

"But what about incomplete features?" That's where feature flags come in.

How It Works

Without Feature Flags

// Can't merge until complete
function checkout(cart: Cart) {
  // 500 lines of new checkout code
  // Still working on payment integration
  // Not ready for production
}

You're stuck in a feature branch until everything is done.

With Feature Flags

// Merge incomplete code safely
function checkout(cart: Cart) {
  if (flags.newCheckoutFlow) {
    return newCheckoutProcess(cart) // New code, still in progress
  }
  return legacyCheckout(cart) // Existing code, production-safe
}

Now you can merge daily, even with incomplete code.

The Daily Workflow

Morning

  1. Pull latest main
  2. Write code
  3. Commit to main (with flag around incomplete work)
  4. Push to main

Afternoon

  1. Pull latest main
  2. Continue work on same feature
  3. Commit to main
  4. Push to main

No branches. No merge conflicts. No waiting.

Benefits You'll Actually Feel

1. Tiny Diffs

When you commit daily, pull requests are small. Instead of:

❌ "Please review: New checkout flow (2,847 lines changed)"

You get:

✅ "Add cart validation for new checkout (87 lines changed)"
✅ "Implement payment token generation (43 lines changed)"
✅ "Add checkout confirmation screen (92 lines changed)"

Small PRs get reviewed quickly and thoroughly.

2. No Merge Conflicts

When everyone works on main and pulls daily, merge conflicts become rare. You're never more than a day out of sync.

3. Continuous Integration (For Real)

Your code integrates with everyone else's code every day. Integration problems are caught immediately, not after weeks of divergent development.

4. Faster Feedback

Commit messy code at 4 PM with a flag around it. Get feedback from teammates the next morning. Refactor based on feedback. You're learning continuously instead of in batch mode.

5. Psychological Safety

No more "point of no return" where your branch has diverged so much that merging is terrifying. Every commit is a small, safe step.

Handling Complex Features

"But our features take weeks to build. How do we trunk-based develop?"

Strategy 1: Incremental Building

Break the feature into small, independently valuable pieces:

Week 1: Basic UI (flagged off) Week 2: Data layer (flagged off) Week 3: Business logic (flagged off) Week 4: Integration (flagged off) Week 5: Testing (flagged on for team) Week 6: Rollout (gradually enabled)

Each week merges to main. No feature branch.

Strategy 2: Branch by Abstraction

Refactor the old code to use an interface, then build the new implementation behind that interface:

// Step 1: Extract interface (merge to main)
interface CheckoutProcessor {
  process(cart: Cart): Promise<Order>
}

// Step 2: Make existing code implement interface (merge to main)
class LegacyCheckout implements CheckoutProcessor {
  // Existing code
}

// Step 3: Build new implementation (merge to main, flagged off)
class NewCheckout implements CheckoutProcessor {
  // New code
}

// Step 4: Use flags to switch implementations (merge to main)
const processor = flags.newCheckout ? new NewCheckout() : new LegacyCheckout()

At every step, code is in main and deployable.

Strategy 3: Dark Launching

Ship the feature to production completely hidden:

An application architecture with a new module behind a gate, illustrating deployed code hidden from users until release.

// Week 1-4: Build feature, ship to production, but completely hidden
if (false) {
  // Will never execute
  return newFeature()
}

// Week 5: Enable for internal testing
if (flags.newFeature && user.isInternal) {
  return newFeature()
}

// Week 6: Enable for everyone
if (flags.newFeature) {
  return newFeature()
}

Common Objections

"Our code review process requires branches"

Many code review tools work great with trunk-based development. Create short-lived branches that exist only for review (< 1 day), or use tools that support reviewing commits directly on main.

"We need stabilization branches for releases"

If you're deploying from main multiple times per day and using flags to control feature visibility, you don't need stabilization branches. Main is always stable because features can be toggled off.

"What about breaking changes?"

Use feature flags to gate breaking changes. When both old and new code paths work, you can safely refactor.

"This won't work for regulated industries"

It works even better. You can prove every change is independently deployable and reversible, which auditors love.

Making the Transition

If you're doing Git Flow today:

Week 1: Start Small

Pick one small feature. Wrap it in a flag. Merge to main daily.

Week 2: Expand

Two features with flags, both merging daily.

Week 3: Team Discussion

Review how it's going. Discuss concerns. Adjust process.

Month 2: New Default

Make trunk-based development the team's default approach.

Month 3: Branch Deprecation

Establish a rule: No branches over 24 hours old.

Metrics to Track

  • Branch lifetime: Target < 24 hours
  • Merge frequency: Target 1+ merges per developer per day
  • PR size: Target < 200 lines changed
  • Merge conflicts: Should drop dramatically
  • Time to integrate: From days to hours

The Elite Performers' Secret

Research from the State of DevOps Report consistently shows: elite performers merge to main multiple times per day. This seemed impossible before feature flags.

Now it's not just possible - it's practical.

Start Tomorrow

You don't need perfect infrastructure or complete buy-in. Start with:

  1. Your next feature
  2. Wrap incomplete code in a flag
  3. Commit to main daily
  4. Experience the difference

After a week, you'll wonder why you ever used long-lived branches.

Trunk-based development isn't a radical idea - it's just software development without the arbitrary constraint of needing features to be "complete" before merging. Feature flags remove that constraint.

Welcome to continuous integration. Actual continuous integration.