Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Feature Flags (Feature Toggles)

Overview

A feature flag (or feature toggle) is a mechanism that allows you to enable or disable application features at runtime without deploying new code. Feature flags decouple deployment from release, enabling safer rollouts, targeted delivery, and rapid experimentation.

graph LR
    subgraph "Without Feature Flags"
        Code[Code + Feature] --> Deploy[Deploy] --> Release[Release]
    end
    subgraph "With Feature Flags"
        Code2[Code + Flag] --> Deploy2[Deploy<br/>Feature OFF] 
        Deploy2 --> Release2[Release<br/>Flip Flag ON]
    end

Why Feature Flags?

BenefitDescription
Decouple deploy from releaseDeploy code dormant, activate when ready
Trunk-based developmentMerge incomplete features behind flags
Targeted rolloutsEnable for specific users, segments, regions
Instant rollbackTurn off a feature without redeploying
A/B testingRoute users to different implementations
Kill switchesDisable problematic features in production
Gradual rollout1% → 10% → 50% → 100%

Flag Types

Classification by Lifecycle

TypePurposeLifecycleExample
Release flagManage feature rolloutShort-lived (hours to weeks)New checkout flow
Experiment flagA/B test different implementationsShort-lived (test duration)Two recommendation algorithms
Ops flagOperational control / kill switchLong-livedDisable expensive background job
Permission flagEntitlement-based accessLong-livedPremium feature for paid users

Classification by Persistence

TypeStored WhereEvaluated When
Static/ConfigConfig file, environment variableApplication startup
Dynamic/APIFeature flag serviceEvery request (cached)
CompiledBuild-time constantCompile time

Implementation

Simple Implementation (Config File)

# config/flags.yaml
features:
  new_checkout:
    enabled: true
    rollout_percentage: 25
    allowed_countries: ["us", "uk"]

# feature_flags.py
import yaml

class FeatureFlags:
    def __init__(self, config_path="config/flags.yaml"):
        with open(config_path) as f:
            self._config = yaml.safe_load(f)["features"]
    
    def is_enabled(self, flag_name, user_context=None):
        flag = self._config.get(flag_name, {})
        if not flag.get("enabled", False):
            return False
        
        # Rollout percentage check
        rollout = flag.get("rollout_percentage", 100)
        if user_context and rollout < 100:
            user_hash = hash(user_context["user_id"]) % 100
            return user_hash < rollout
        
        # Country check
        allowed = flag.get("allowed_countries")
        if user_context and allowed:
            return user_context.get("country") in allowed
        
        return True

flags = FeatureFlags()

Production-Grade: Feature Flag Service

graph TB
    App[Application] -->|1. Check flag| SDK[Feature Flag SDK]
    SDK -->|2. Local cache| Cache[In-Memory Cache<br/>~1s TTL]
    SDK -->|3. Cache miss| API[Flag Service API]
    API -->|4. Evaluate rules| Rules[Rule Engine]
    API -->|5. Store| DB[Flag Configuration Store]
    Rules --> Segment[User Segments]
    Rules --> Targeting[Targeting Rules]
    Rules --> Gradual[Gradual Rollout]

Usage in Application Code

// Clean: flag wraps the behavior
public OrderSummary getOrderSummary(User user, Order order) {
    if (featureFlags.isEnabled("new-pricing-engine", user)) {
        return newPricingEngine.calculate(user, order);
    }
    return legacyPricingEngine.calculate(user, order);
}

Bad: flags scattered everywhere with complex nested logic. Good: flags at decision boundaries, clean if/else, one flag per feature.

Targeting and Segmentation

Targeting MethodDescriptionExample
User IDSpecific usersBeta testers, employees
User segmentGroup-based attributesPremium users, enterprise
Percentage rolloutDeterministic hash-based5% of users
GeographyCountry, region, cityEU-only for GDPR compliance
Device/PlatformOS, browser, app versioniOS first, Android later
Custom attributesAny user propertyUsers with >100 orders
EnvironmentStaging, productionTest in production with real traffic

Deterministic Rollout

Percentage rollouts must be deterministic — the same user always gets the same result:

import hashlib

def should_see_feature(user_id, percentage):
    """Deterministic: same user always gets same result"""
    hash_value = int(hashlib.md5(user_id.encode()).hexdigest(), 16)
    return (hash_value % 100) < percentage

# User "abc" at 25%: hash("abc") % 100 = 42 → False (not in first 25%)
# User "abc" at 50%: hash("abc") % 100 = 42 → True (in first 50%)
# This never changes for the same user_id

Gradual Rollout Workflow

stateDiagram-v2
    [*] --> Dev: Develop behind flag
    Dev --> QA: Flag ON in staging
    QA --> Internal: Flag ON for employees
    Internal --> Canary: 1% of users
    Canary --> Expand: 10% → 25% → 50%
    Expand --> Full: 100%
    Full --> Cleanup: Remove flag, deploy
    Cleanup --> [*]

Comparison: Feature Flag Services

FeatureLaunchDarklyFlagsmithUnleashFlipt
HostingSaaS onlySaaS + self-hostedSelf-hostedSelf-hosted
Open sourceNoYes (API)YesYes
PricingEnterprise (USD)Free tier + paidFreeFree
SDKs15+ languages8+10+gRPC, REST
A/B testingYesNoNoNo
Gradual rolloutYesYesYesYes
Targeting rulesRichRichRichBasic
Best forEnterprise, experimentsSimple teams, self-hostSelf-hosted, K8s-nativeSimple, self-hosted

Anti-Patterns

Anti-PatternWhy It’s BadFix
Flag sprawlHundreds of flags, unclear which are activeAudit regularly, remove dead flags
Long-lived release flagsCode branches in production foreverTimebox: remove within 2 weeks of full rollout
Deep nestingif flagA and flagB and flagCOne flag per feature; compose in the rule engine
No defaultif flag: new_code without elseAlways handle both code paths
Flags in hot loopsChecking flag on every iterationCache flag evaluation at request start
No monitoringDon’t know which flags are being evaluatedLog flag evaluations, monitor flag service latency

Cleanup Strategy

Dead flags accumulate technical debt. Implement a cleanup process:

  1. Tag every flag with creation date and owner
  2. Set an expiry date (e.g., 90 days for release flags)
  3. Automated alerts when flags exceed their expiry
  4. Periodic audit (monthly): identify and remove dead flags
  5. Remove the flag from code after full rollout (don’t just leave it true)
# Bad: flag left in forever
if feature_flags.is_enabled("new_checkout"):  # Always True
    new_checkout()
else:
    old_checkout()  # Dead code

# Good: flag removed after full rollout
new_checkout()  # Just the new code

Interview Questions

  1. How do feature flags relate to trunk-based development? Feature flags allow developers to merge incomplete or risky features into the main branch behind a flag. The code is deployed but dormant. This eliminates long-lived feature branches and reduces merge conflicts.

  2. What happens if the feature flag service is down? SDKs use local caching with a fallback value (usually the last known state). This means flags continue to work during outages, but you can’t change flag values until the service recovers.

  3. How do you prevent feature flag sprawl? Track every flag in a registry with owner, creation date, type, and expiry. Automate alerts for stale flags. Make cleanup a part of the deployment checklist. Limit the number of active flags per service.

  4. What’s the difference between a feature flag and a canary release? A feature flag controls which users see a feature within the same deployed version. A canary release deploys a new version of the entire service and routes traffic to it. They’re complementary: use flags for feature-level control and canaries for infrastructure-level safety.

  5. How would you implement feature flags for a mobile app? Use a remote config/flag SDK that fetches flags on app launch and caches locally. Handle offline scenarios by using the cached value. For critical flags, fetch on every app foreground. Consider using Firebase Remote Config or a similar mobile-first solution.

Key Takeaways

  • Feature flags decouple deployment from release, enabling safer and more flexible software delivery
  • Four main types: release flags (short-lived), experiment flags, ops flags, and permission flags (long-lived)
  • Deterministic percentage rollout (hash-based) ensures the same user always sees the same version
  • Clean up release flags after full rollout — dead flags accumulate technical debt
  • Feature flag services (LaunchDarkly, Unleash, Flagsmith) provide SDKs, targeting rules, and dashboards
  • Always handle both code paths (flag on/off) and monitor flag service health
  • Combine with canary releases: flags for feature-level, canaries for infrastructure-level safety

Cross-References