Feature Flags Pattern

Feature Flags Pattern

"A feature toggle (also known as a feature flag) is a technique in software development that allows teams to enable or disable features without deploying new code." -- Hodgson, "Feature Toggles (aka Feature Flags)", martinfowler.com

Intent

Feature Flags implement Strategy at deployment level -- the flag selects which Strategy implementation is active at runtime. Where GoF Strategy uses setStrategy() to swap an algorithm object at runtime, Feature Flags use a flag evaluation call to determine which code path executes. The flag IS the setStrategy() call: router.isEnabled('new-checkout-flow', ctx) selects between the old and new checkout implementations without a code deployment.

Feature Flags decouple deployment from release. Code is deployed to production but remains dormant until a flag enables it. This separation makes trunk-based development viable at scale: all engineers commit to the main branch continuously; incomplete features are hidden behind flags rather than isolated in long-lived feature branches. A flag is removed when the rollout is complete -- its lifecycle mirrors a temporary code smell, not a permanent configuration value.

The Strategy lineage chain for deployment patterns: Strategy-Pattern (GoF ancestor, object-level algorithm swap) -> Feature Flags (deployment-level strategy selection) -> Canary-Release-Pattern (flags enable traffic splitting to a new version) -> Blue-Green-Deployment (flags used for pre-switch validation before router cutover).

When NOT to Use

  • All features are mandatory for all users -- no segmentation or gradual rollout is needed, so flag infrastructure adds overhead with no benefit
  • Team lacks discipline for flag cleanup -- flag debt accumulates and the codebase gradually fills with dead code paths and stale conditional branches
  • System has fewer than 3 deployments per quarter -- deployment frequency is too low to justify flag infrastructure and the associated TTL management overhead
  • Feature is a one-time data migration -- use a deployment strategy (Blue/Green, Rolling) with the Expand/Contract migration pattern, not a flag

When to Use

  • Trunk-based development with continuous deployment -- flags replace feature branches as the isolation mechanism
  • Canary or A/B testing rollout needed -- flags route a percentage of users to the new feature variant (see Canary-Release-Pattern)
  • Kill-switch capability required for production features -- ops flags enable instant disablement without a redeployment
  • Per-user entitlement features -- permission flags enable features for specific user tiers, beta groups, or subscription levels

How It Works

The flag evaluation flow has three participants:

  • Flag Store -- the source of flag state (name, enabled, rollout percentage, targeting rules). In production: LaunchDarkly, Unleash, Flagsmith, or AWS AppConfig. In the skeletal example below: an in-memory Map.
  • FeatureFlagRouter (Context) -- evaluates flag state against an evaluation context (user ID, environment). Implements deterministic hash bucketing so the same user always receives the same treatment (avoids flickering between A and B).
  • Evaluation Context -- user ID, environment, and any targeting attributes. The router uses this context to decide whether the flag is active for the requesting user.

The client calls router.isEnabled(flagName, context) and routes to the appropriate code path based on the boolean result. This is Strategy pattern's context.execute() applied at the deployment level.

Hodgson Flag Taxonomy

TypePurposeTTL
ReleaseEnable feature for gradual rollout; removed after full rollout30 days max
ExperimentA/B testing variants; removed after experiment concludes90 days max
OpsCircuit breaker / kill switch; long-lived but reviewed quarterlySemi-permanent (quarterly review)
PermissionPer-user entitlement; tied to subscription or role lifecycleLong-lived (tied to subscription)

Flag Debt / TTL Guidance

Pitfall -- Flag Debt: A team adds 50 feature flags over 18 months. No one tracks which are still active. Removing any flag requires archaeology -- reading code to determine if the flag is still checked and what the old codepath does. Some flags have been "permanent" for a year despite being temporary by design.

Every flag must have a documented TTL (time-to-live) or removal date recorded at creation time. Release and experiment flags accumulate indefinitely without TTL enforcement. The canonical approach is a flag registry with four fields per entry: flag name, owner, creation date, removal date. Treat flag removal as a first-class task: add a flag-removal issue or story to the same sprint that completes the feature rollout. The flag's dead code path must be deleted, not just the flag check.

Recommended per-type enforcement:

  • Release flags: Remove within 30 days of full rollout confirmation
  • Experiment flags: Remove within 90 days of experiment conclusion regardless of result
  • Ops flags: Review quarterly; document the condition that would allow removal
  • Permission flags: Tie removal to subscription tier lifecycle; document in the product ownership model

Evaluation Flow Diagram

flowchart TD
    REQ[Client Request<br/>userId + environment]
    REQ --> ROUTER[FeatureFlagRouter<br/>isEnabled flagName, context]

    ROUTER --> STORE[(Flag Store<br/>LaunchDarkly / Unleash /<br/>in-memory)]
    STORE -->|flag config: enabled,<br/>rollout %, targeting rules| ROUTER

    ROUTER --> CHECK{Flag Enabled<br/>for this user?}

    CHECK -->|"Targeting rule match<br/>OR hash bucket <= rollout %"| NEW[New Code Path<br/>feature ON]
    CHECK -->|"No match<br/>AND hash bucket > rollout %"| OLD[Old Code Path<br/>feature OFF]

    subgraph Taxonomy["Hodgson Flag Types"]
        direction LR
        REL["Release<br/>TTL: 30d"]
        EXP["Experiment<br/>TTL: 90d"]
        OPS["Ops<br/>quarterly review"]
        PERM["Permission<br/>subscription-tied"]
    end

    ROUTER -.->|"flag type determines<br/>lifecycle + TTL"| Taxonomy

    style CHECK fill:#fff3e6,stroke:#d98c00
    style NEW fill:#e6ffe6,stroke:#4a9d4a
    style OLD fill:#ffe6e6,stroke:#d94a4a
    style Taxonomy fill:#f0f4ff,stroke:#6a6a9a

TypeScript Example

// Feature Flags Pattern — TypeScript
// Source: Hodgson, "Feature Toggles (aka Feature Flags)", martinfowler.com
// GoF ancestor: Strategy pattern — the flag selects the active Strategy at runtime.
 
interface FeatureFlag {
  name: string;
  enabled: boolean;
  rolloutPercentage?: number; // 0-100; undefined = 100% if enabled
}
 
interface FlagEvaluationContext {
  userId: string;
  environment: 'production' | 'staging' | 'development';
}
 
// Flag router — the "Context" from Strategy pattern
class FeatureFlagRouter {
  constructor(private flags: Map<string, FeatureFlag>) {}
 
  isEnabled(flagName: string, ctx: FlagEvaluationContext): boolean {
    const flag = this.flags.get(flagName);
    if (!flag || !flag.enabled) return false;
    if (flag.rolloutPercentage === undefined) return true;
    // Deterministic hash: same user always gets same treatment
    const bucket = this.hashUser(ctx.userId) % 100;
    return bucket < flag.rolloutPercentage;
  }
 
  private hashUser(userId: string): number {
    return userId.split('').reduce((acc, c) => acc + c.charCodeAt(0), 0);
  }
}
 
// Usage: flag selects strategy (GoF Strategy lineage)
const router = new FeatureFlagRouter(new Map([
  ['new-checkout-flow', { name: 'new-checkout-flow', enabled: true, rolloutPercentage: 10 }]
]));
 
const useNew = router.isEnabled('new-checkout-flow', { userId: 'user-42', environment: 'production' });
// PRODUCTION NOTE: replace Map with LaunchDarkly / Unleash SDK for distributed flag state,
// real-time updates, targeting rules, and audit log.
// TTL: remove flag and dead codepath within 30 days of full rollout completion.

Java Example

// Feature Flags Pattern — Java
// Source: Hodgson, "Feature Toggles (aka Feature Flags)", martinfowler.com
// GoF ancestor: Strategy pattern selects the active variant at runtime.
 
public class FeatureFlagRouter {
 
    private final Map<String, FeatureFlag> flags;
 
    public FeatureFlagRouter(Map<String, FeatureFlag> flags) {
        this.flags = flags;
    }
 
    public boolean isEnabled(String flagName, String userId) {
        FeatureFlag flag = flags.get(flagName);
        if (flag == null || !flag.isEnabled()) return false;
        if (flag.getRolloutPercentage() == null) return true;
        int bucket = Math.abs(userId.hashCode()) % 100;
        return bucket < flag.getRolloutPercentage();
    }
}
 
// record FeatureFlag(String name, boolean enabled, Integer rolloutPercentage) {}
// PRODUCTION NOTE: replace Map with Unleash Java SDK or LaunchDarkly Java SDK.
// TTL: every release flag must have a removal issue created at flag creation time.

Lineage Backward

Strategy-Pattern (GoF ancestor) -- Feature Flags is Strategy applied at the deployment level. The flag is the runtime setStrategy() call. The FeatureFlagRouter is the Context; the flag name is the strategy selector; the two code paths behind the flag are the ConcreteStrategies. GoF Strategy operates within a single process at the object level; Feature Flags operate across the deployment pipeline at the release level.

Lineage Forward

  • Canary-Release-Pattern -- Feature Flags enable Canary traffic splitting: a release flag with rolloutPercentage: 10 routes 10% of users to the new version. The flag percentage is adjusted incrementally as confidence grows.
  • Blue-Green-Deployment -- Feature Flags used for pre-switch validation: the new feature is deployed to Green behind a flag, validated for a subset of users, then the router switches from Blue to Green and the flag is removed.
PatternRelationship
Strategy-PatternGoF ancestor -- flag selects active strategy; FeatureFlagRouter is the Context
Canary-Release-PatternConsumer -- Canary uses flags for traffic splitting by user percentage
Blue-Green-DeploymentConsumer -- flags validate Green before router switch
12-Factor-AppFactor III (Config) enables externalized flag state -- flags are config, not code
  • Micro-Frontends — Feature Flags enable progressive micro-frontend rollout: a new micro-frontend can be deployed and enabled for a subset of users before becoming the default, enabling canary-style frontend releases.

Sources

  • Hodgson, Pete. "Feature Toggles (aka Feature Flags)" -- martinfowler.com/articles/feature-toggles.html -- comprehensive flag taxonomy (release/experiment/ops/permission), TTL guidance; primary source
  • Fowler, Martin. "FeatureToggles" -- martinfowler.com/bliki/FeatureToggles.html -- taxonomy (primary reference)
  • LaunchDarkly documentation -- launchdarkly.com/docs -- implementation reference for production flag management

Notes that link here: Strategy-Pattern, Canary-Release-Pattern, Blue-Green-Deployment, 12-Factor-App