Tips and Tricks – Implement Trunk-Based Development with Feature Flags

Feature branches create merge nightmares. Develop for weeks
in isolation? Integration hell on merge day. GitFlow with long-lived branches? Conflicts, broken builds, deployment
delays. Trunk-based development with feature flags solves this by keeping everyone on main while hiding incomplete
features behind toggles—transforming terror merges into continuous integration.

This guide covers production-ready trunk-based development
with feature flags that enable continuous deployment without feature branch chaos. We’ll ship to production daily
while keeping incomplete features hidden.

Why Feature Flags Transform Development

The Feature Branch Problem

Long-lived feature branches suffer from:

  • Merge conflicts: Weeks of divergence create integration nightmares
  • Integration delays: Features sit unmerged for weeks
  • Broken main: Large merges break builds
  • Deployment blocks: Can’t deploy until feature complete
  • Stale branches: Diverge from main, become unmergeable
  • Code review bottlenecks: Huge PRs impossible to review

Feature Flags Benefits

  • Merge daily: Always integrate with main
  • Deploy incomplete features: Hide behind flags
  • Gradual rollouts: 1% → 10% → 100%
  • Instant rollback: Toggle flag off if issues
  • A/B testing: Different users see different features
  • Small PRs: Merge frequently, review easily

Pattern 1: Basic Feature Flags

Simple Boolean Toggles

// Feature flag service
class FeatureFlagService {
    private flags: Map<string, boolean> = new Map();
    
    constructor() {
        // Load from config/database
        this.flags.set('new-checkout-flow', false);
        this.flags.set('enhanced-search', true);
        this.flags.set('ai-recommendations', false);
    }
    
    isEnabled(flagName: string): boolean {
        return this.flags.get(flagName) ?? false;
    }
    
    enable(flagName: string): void {
        this.flags.set(flagName, true);
    }
    
    disable(flagName: string): void {
        this.flags.set(flagName, false);
    }
}

// Usage in code
const flags = new FeatureFlagService();

// Old code path (production)
function processCheckout(cart: Cart) {
    if (flags.isEnabled('new-checkout-flow')) {
        // New implementation (behind flag)
        return processCheckoutV2(cart);
    }
    
    // Old implementation (default)
    return processCheckoutV1(cart);
}

// Hide UI elements
function renderDashboard() {
    return (
        <div>
            <h1>Dashboard</h1>
            
            {/* Always visible */}
            <OrderHistory />
            
            {/* Feature flag controlled */}
            {flags.isEnabled('ai-recommendations') && (
                <AIRecommendations />
            )}
        </div>
    );
}

// Benefits:
// - Deploy incomplete AI recommendations to production
// - Toggle on when ready
// - Instant rollback if issues

Pattern 2: Percentage Rollouts

Gradual Feature Release

from typing import Optional
import hashlib

class FeatureFlags:
    def __init__(self):
        self.flags = {
            'new-algorithm': {
                'enabled': True,
                'percentage': 10  # 10% of users
            },
            'premium-features': {
                'enabled': True,
                'percentage': 100,
                'whitelist': ['user_123', 'user_456']  # Always on for these
            }
        }
    
    def is_enabled(self, flag_name: str, user_id: str) -> bool:
        if flag_name not in self.flags:
            return False
        
        flag = self.flags[flag_name]
        
        # Check if globally disabled
        if not flag.get('enabled', False):
            return False
        
        # Check whitelist
        if user_id in flag.get('whitelist', []):
            return True
        
        # Check percentage rollout
        percentage = flag.get('percentage', 0)
        
        if percentage >= 100:
            return True
        
        if percentage <= 0:
            return False
        
        # Consistent hashing for stable rollout
        hash_input = f"{flag_name}:{user_id}".encode()
        hash_value = int(hashlib.md5(hash_input).hexdigest(), 16)
        user_percentage = hash_value % 100
        
        return user_percentage < percentage
    
    def set_percentage(self, flag_name: str, percentage: int):
        """Gradually increase rollout"""
        if flag_name in self.flags:
            self.flags[flag_name]['percentage'] = percentage

# Usage
flags = FeatureFlags()

def get_search_results(query: str, user_id: str):
    if flags.is_enabled('new-algorithm', user_id):
        # 10% of users get new algorithm
        return enhanced_search(query)
    else:
        # 90% get old algorithm
        return legacy_search(query)

# Gradual rollout strategy:
# Day 1: 1% (test with small audience)
flags.set_percentage('new-algorithm', 1)

# Day 2: No issues, increase to 10%
flags.set_percentage('new-algorithm', 10)

# Day 5: Looking good, 50%
flags.set_percentage('new-algorithm', 50)

# Day 7: Full rollout
flags.set_percentage('new-algorithm', 100)

# If issues at any point: rollback to 0% instantly!

Pattern 3: User Targeting

Feature Flags with Attributes

public class UserContext
{
    public string UserId { get; set; }
    public string Email { get; set; }
    public string Country { get; set; }
    public string Tier { get; set; }  // free, premium, enterprise
    public bool IsBetaTester { get; set; }
}

public class FeatureFlagService
{
    public bool IsEnabled(string flagName, UserContext user)
    {
        return flagName switch
        {
            // Enable for beta testers only
            "experimental-ui" => user.IsBetaTester,
            
            // Enable for premium users
            "advanced-analytics" => user.Tier == "premium" || user.Tier == "enterprise",
            
            // Regional rollout
            "eu-compliance-mode" => user.Country == "DE" || user.Country == "FR",
            
            // Specific user whitelist
            "admin-panel" => IsInWhitelist(user.UserId, "admin-panel"),
            
            // Complex targeting
            "new-payment-flow" => EvaluateComplexRule(flagName, user),
            
            _ => false
        };
    }
    
    private bool EvaluateComplexRule(string flagName, UserContext user)
    {
        // Get targeting rules from database/config
        var rules = GetTargetingRules(flagName);
        
        return rules.Any(rule => rule.Matches(user));
    }
}

// Targeting rule example
public class TargetingRule
{
    public string Attribute { get; set; }  // "country", "tier", etc.
    public string Operator { get; set; }   // "equals", "in", "contains"
    public List<string> Values { get; set; }
    
    public bool Matches(UserContext user)
    {
        var userValue = GetUserAttribute(user, Attribute);
        
        return Operator switch
        {
            "equals" => Values.Contains(userValue),
            "in" => Values.Contains(userValue),
            "contains" => Values.Any(v => userValue?.Contains(v) ?? false),
            "not_equals" => !Values.Contains(userValue),
            _ => false
        };
    }
}

// Usage
var user = new UserContext
{
    UserId = "user_123",
    Country = "US",
    Tier = "premium",
    IsBetaTester = true
};

if (_flags.IsEnabled("advanced-analytics", user))
{
    // Premium feature
    return GetAdvancedAnalytics();
}

Pattern 4: Feature Flag Frameworks

LaunchDarkly/Unleash Integration

// Using LaunchDarkly SDK
const LaunchDarkly = require('launchdarkly-node-server-sdk');

const ldClient = LaunchDarkly.init(process.env.LAUNCHDARKLY_SDK_KEY);

// Wait for initialization
await ldClient.waitForInitialization();

// User context
const user = {
    key: 'user_123',
    email: 'user@example.com',
    country: 'US',
    custom: {
        tier: 'premium',
        signupDate: '2023-01-15'
    }
};

// Check flag
const showNewFeature = await ldClient.variation(
    'new-checkout-flow',
    user,
    false  // Default if flag not found
);

if (showNewFeature) {
    // New feature code
} else {
    // Old feature code
}

// Multi-variate flags (not just boolean)
const checkoutVersion = await ldClient.variation(
    'checkout-version',
    user,
    'v1'  // Default version
);

switch (checkoutVersion) {
    case 'v1':
        return renderCheckoutV1();
    case 'v2':
        return renderCheckoutV2();
    case 'v3-experimental':
        return renderCheckoutV3();
}

// Track metrics for A/B testing
ldClient.track('checkout-completed', user, {
    revenue: order.total,
    items: order.items.length
});

// LaunchDarkly dashboard shows:
// - V1: 5% conversion, $45 avg
// - V2: 7% conversion, $52 avg (winner!)
// - V3: 3% conversion, $38 avg (needs work)

Pattern 5: Kill Switches

Emergency Feature Disable

@Service
public class OrderService {
    
    @Autowired
    private FeatureFlagService featureFlags;
    
    public Order createOrder(CreateOrderRequest request) {
        // Kill switch for new inventory system
        if (featureFlags.isEnabled("use-new-inventory-service")) {
            try {
                return createOrderWithNewInventory(request);
            } catch (Exception e) {
                log.error("New inventory system failed, falling back", e);
                
                // Auto-disable flag on repeated failures
                featureFlags.disableOnError("use-new-inventory-service", e);
                
                // Fall through to old system
            }
        }
        
        // Old reliable system
        return createOrderWithLegacyInventory(request);
    }
    
    // Circuit breaker pattern with feature flags
    public void processPayment(Payment payment) {
        String provider = featureFlags.getStringValue(
            "payment-provider",
            "stripe"  // Default
        );
        
        // Can switch payment providers instantly via flag
        switch (provider) {
            case "stripe":
                return stripeService.process(payment);
            case "braintree":
                return braintreeService.process(payment);
            case "fallback":
                // Emergency fallback if both fail
                return manualProcessing.queue(payment);
        }
    }
}

// Kill switch service
@Service
public class KillSwitchService {
    
    @Scheduled(fixedRate = 10000) // Every 10 seconds
    public void monitorSystemHealth() {
        // Check error rates
        double errorRate = getErrorRate("new-inventory-service");
        
        if (errorRate > 0.05) {  // 5% error rate
            log.warn("High error rate detected, disabling new inventory");
            
            featureFlags.disable("use-new-inventory-service");
            
            // Alert team
            slack.sendAlert("🚨 New inventory disabled due to high errors");
        }
    }
}

Pattern 6: Trunk-Based Workflow

Development Process

# Trunk-based development workflow

# Day 1: Start new feature
git checkout main
git pull
git checkout -b feature/new-search

# Add feature flag (always first commit!)
# config/features.json
{
  "enhanced-search": {
    "enabled": false,
    "description": "New search algorithm with AI",
    "created": "2024-01-15",
    "owner": "search-team"
  }
}

# Commit 1: Add flag configuration
git add config/features.json
git commit -m "feat: add enhanced-search feature flag"
git push origin feature/new-search
# Create PR, merge to main (✓ safe, flag is off)

# Day 2: Add new code behind flag
# src/search.ts
export function search(query: string): Results {
    if (flags.isEnabled('enhanced-search')) {
        return enhancedSearch(query);  // New code
    }
    return legacySearch(query);  // Old code
}

# Commit 2: Implement new search
git add src/search.ts
git commit -m "feat: implement enhanced search (behind flag)"
git push
# Create PR, merge to main (✓ safe, flag still off)

# Day 3: Add tests
# tests/search.test.ts
test('enhanced search finds more results', () => {
    flags.enable('enhanced-search');
    const results = search('machine learning');
    expect(results.length).toBeGreaterThan(10);
});

# Commit 3: Add tests
git add tests/
git commit -m "test: add enhanced search tests"
git push
# Merge to main (✓ safe, tested)

# Day 4: Deploy to production
# Feature is deployed but hidden behind flag!
# No users see it yet

# Day 5: Internal testing
# Enable for team only
{
  "enhanced-search": {
    "enabled": true,
    "percentage": 0,
    "whitelist": ["team@company.com"]
  }
}

# Day 6: Gradual rollout
# 1% of users
flags.setPercentage('enhanced-search', 1);

# Day 7: Monitor metrics
# No issues, increase to 10%
flags.setPercentage('enhanced-search', 10);

# Day 10: Full rollout
flags.setPercentage('enhanced-search', 100);

# Day 15: Remove flag (cleanup)
# Once stable, remove flag and old code
git checkout -b cleanup/remove-enhanced-search-flag
# Remove flag checks, delete old code
git commit -m "refactor: remove enhanced-search flag (100% rollout)"

# Benefits:
# - Merged to main daily
# - Deployed to production daily
# - No merge conflicts
# - Easy rollback (toggle flag)
# - Small reviewable PRs

Real-World Example: E-Commerce Checkout Redesign

Complete Feature Flag Strategy

// Feature flag configuration
interface CheckoutFlags {
    'checkout-v2-enabled': boolean;
    'checkout-v2-percentage': number;
    'checkout-express-enabled': boolean;
    'checkout-payment-icons': boolean;
}

class CheckoutFeatureFlags {
    async getCheckoutVersion(userId: string): Promise<'v1' | 'v2'> {
        // Check if v2 enabled globally
        if (!await this.isEnabled('checkout-v2-enabled')) {
            return 'v1';
        }
        
        // Check percentage rollout
        const percentage = await this.getPercentage('checkout-v2-percentage');
        const userHash = this.hashUser(userId);
        
        if (userHash < percentage) {
            return 'v2';
        }
        
        return 'v1';
    }
}

// Usage in checkout flow
async function renderCheckout(userId: string) {
    const version = await flags.getCheckoutVersion(userId);
    
    if (version === 'v2') {
        // New checkout with feature flags for sub-features
        return (
            <CheckoutV2
                showExpressCheckout={await flags.isEnabled('checkout-express-enabled', userId)}
                showPaymentIcons={await flags.isEnabled('checkout-payment-icons', userId)}
            />
        );
    }
    
    // Old checkout (stable fallback)
    return <CheckoutV1 />;
}

// Track conversion metrics
async function onCheckoutComplete(userId: string, order: Order) {
    const version = await flags.getCheckoutVersion(userId);
    
    // Send to analytics
    analytics.track('checkout_completed', {
        userId,
        orderId: order.id,
        checkoutVersion: version,
        revenue: order.total,
        items: order.items.length
    });
}

// Rollout plan:
// Week 1: Internal (whitelist)
// Week 2: 1% of users
// Week 3: 10% of users  
// Week 4: 50% of users
// Week 5: 100% of users
// Week 6: Remove flag, delete v1 code

Best Practices

  • Flag first: Add flag before implementing feature
  • Default to off: New flags should default to disabled
  • Small PRs: Merge to main daily, hide behind flags
  • Gradual rollout: 1% → 10% → 50% → 100%
  • Monitor metrics: Track conversion, errors by flag state
  • Clean up flags: Remove after 100% rollout (set deadline)
  • Document flags: Owner, purpose, cleanup date

Common Pitfalls

  • Flag sprawl: Hundreds of old flags never removed
  • No cleanup plan: Flags become permanent
  • Testing only one path: Test both flag on and off
  • Nested flags: Complex flag dependencies
  • No monitoring: Can't tell if flag caused issues
  • Using for configuration: Use config system, not flags

Key Takeaways

  • Feature flags enable trunk-based development without feature branches
  • Deploy incomplete features to production (hidden behind flags)
  • Gradual rollouts reduce risk: 1% → 10% → 100%
  • Instant rollback: toggle flag off if issues
  • A/B testing built-in: compare metrics by flag state
  • Merge to main daily, ship to production daily
  • Clean up flags after 100% rollout (set cleanup deadline)
  • Use frameworks like LaunchDarkly for advanced targeting

Feature flags transform development from risky big-bang releases into safe, incremental deployments. By
decoupling deployment from release, you ship to production daily while controlling who sees what. The result:
faster delivery, lower risk, and the confidence to deploy anytime. Master trunk-based development with feature
flags, and you'll never fear merge conflicts again.


Discover more from C4: Container, Code, Cloud & Context

Subscribe to get the latest posts sent to your email.

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.