---
# === IDENTITY ===
id: software/patterns/feature-flags/2026
canonical_question: "How do I implement feature flags?"
aliases:
  - "feature toggles implementation"
  - "feature flag best practices"
  - "how to do progressive rollout with feature flags"
  - "feature flag types release experiment ops permission"
  - "feature flag cleanup and technical debt"
  - "LaunchDarkly Unleash feature flag setup"
  - "percentage rollout canary release feature flags"
  - "kill switch pattern"
entity_type: software_reference
domain: software > patterns > feature_flags
region: global
jurisdiction: global
temporal_scope: 2020-2026

# === VERIFICATION ===
last_verified: 2026-02-24
confidence: 0.90
version: 1.0
first_published: 2026-02-24

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "Martin Fowler's feature toggles taxonomy (2017) remains the industry standard; OpenFeature spec v0.7 (2024) standardizes SDK interfaces"
  next_review: 2026-08-23
  change_sensitivity: low

# === CONSTRAINTS ===
constraints:
  - "Never expose flag evaluation logic to the client in security-sensitive contexts -- server-side evaluation prevents users from toggling flags via browser devtools"
  - "Always provide a default fallback value when evaluating a flag -- if the flag service is unreachable, the application must not crash or expose unfinished features"
  - "Never use feature flags to gate security controls (authentication, authorization, encryption) -- a misconfigured flag could disable security entirely"
  - "Remove feature flags within 30 days of full rollout -- stale flags accumulate as technical debt and create untested code paths"
  - "Test both flag-on and flag-off paths in CI -- untested branches become landmines in production"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Need A/B testing with statistical significance analysis"
    use_instead: "A/B testing framework (Optimizely, Google Optimize, Statsig) -- feature flags handle rollout, not experimentation analytics"
  - condition: "Need environment-based configuration (dev/staging/prod)"
    use_instead: "Environment variables or configuration management (dotenv, Vault, AWS SSM) -- feature flags are for runtime feature control, not environment config"
  - condition: "Need role-based access control for application features"
    use_instead: "software/patterns/rbac-implementation/2026 -- RBAC is for permanent permission gating, feature flags are for temporary rollout"

# === AGENT HINTS ===
inputs_needed:
  - key: "flag_service"
    question: "Will you use a managed service (LaunchDarkly, Unleash, Flagsmith) or build a custom implementation?"
    type: choice
    options: ["managed_service", "custom", "not_sure"]
  - key: "language"
    question: "What is your primary backend language?"
    type: choice
    options: ["node_js", "python", "go", "java", "other"]
  - key: "flag_type"
    question: "What type of feature flag do you need?"
    type: choice
    options: ["release_toggle", "experiment_toggle", "ops_toggle", "permission_toggle", "kill_switch"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/patterns/feature-flags/2026"
suggested_citation: "Source: knowledgelib.io -- AI Knowledge Library (verified 2026-02-24)"

# === RELATED UNITS ===
related_kos:
  related_to:
    - id: "software/patterns/api-versioning/2026"
      label: "API Versioning Strategies"
    - id: "software/patterns/database-migration-strategies/2026"
      label: "Database Migration Strategies"
  solves:
    - id: "software/patterns/state-machine-implementation/2026"
      label: "State Machine Implementation"
  alternative_to:
    - id: "software/patterns/rbac-implementation/2026"
      label: "Role-Based Access Control (for permanent feature gating)"
  often_confused_with:
    - id: "software/patterns/rbac-implementation/2026"
      label: "RBAC -- permanent permission gating vs temporary feature rollout"

# === SOURCES (7 authoritative sources) ===
sources:
  - id: src1
    title: "Feature Toggles (aka Feature Flags)"
    author: Pete Hodgson / Martin Fowler
    url: https://martinfowler.com/articles/feature-toggles.html
    type: technical_blog
    published: 2017-10-09
    reliability: authoritative
  - id: src2
    title: "11 Principles for Building and Scaling Feature Flag Systems"
    author: Unleash
    url: https://docs.getunleash.io/guides/feature-flag-best-practices
    type: official_docs
    published: 2024-06-15
    reliability: high
  - id: src3
    title: "Feature Flags Best Practices"
    author: Flagsmith
    url: https://www.flagsmith.com/blog/feature-flags-best-practices
    type: technical_blog
    published: 2024-11-20
    reliability: moderate_high
  - id: src4
    title: "Reducing Technical Debt from Feature Flags"
    author: LaunchDarkly
    url: https://launchdarkly.com/docs/guides/flags/technical-debt
    type: official_docs
    published: 2024-09-10
    reliability: high
  - id: src5
    title: "Feature Flags 101: Use Cases, Benefits, and Best Practices"
    author: LaunchDarkly
    url: https://launchdarkly.com/blog/what-are-feature-flags/
    type: technical_blog
    published: 2024-08-15
    reliability: high
  - id: src6
    title: "The 12 Commandments of Feature Flags"
    author: Octopus Deploy
    url: https://octopus.com/devops/feature-flags/feature-flag-best-practices/
    type: technical_blog
    published: 2025-01-10
    reliability: moderate_high
  - id: src7
    title: "Canary Releases with Feature Flags"
    author: Unleash
    url: https://www.getunleash.io/blog/canary-deployment-what-is-it
    type: technical_blog
    published: 2024-07-22
    reliability: moderate_high
---

# Feature Flags: Implementation Guide

## TL;DR

- **Bottom line**: Feature flags decouple deployment from release -- ship code to production without exposing it to users, then control visibility at runtime.
- **Key tool**: Boolean, percentage, and user-segment flags evaluated server-side with local caching.
- **Watch out for**: Flag debt -- every flag left active past its rollout becomes technical debt. Remove flags within 30 days of full rollout.
- **Works with**: Any language/framework. Managed services (LaunchDarkly, Unleash, Flagsmith) or custom implementations with config stores (database, Redis, environment variables).

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- Never expose flag evaluation logic to the client in security-sensitive contexts -- server-side evaluation prevents users from toggling flags via browser devtools
- Always provide a default fallback value when evaluating a flag -- if the flag service is unreachable, the application must not crash or expose unfinished features
- Never use feature flags to gate security controls (authentication, authorization, encryption) -- a misconfigured flag could disable security entirely
- Remove feature flags within 30 days of full rollout -- stale flags accumulate as technical debt and create untested code paths [src4]
- Test both flag-on and flag-off paths in CI -- untested branches become landmines in production [src2]

## Quick Reference

| Flag Type | Complexity | Typical Lifetime | Use Case | Cleanup |
|---|---|---|---|---|
| Release Toggle | Low | Days to weeks | Gradually roll out a new feature to users | Remove after 100% rollout confirmed stable |
| Experiment Toggle | Medium | Weeks to months | A/B test two variants and measure outcomes | Remove after experiment concludes and winner ships |
| Ops Toggle | Low | Permanent or long-lived | Circuit breaker, graceful degradation under load | Keep as long as operational risk exists |
| Permission Toggle | Medium | Months to years | Gate premium features by user tier/plan | Keep for lifetime of pricing model |
| Kill Switch | Low | Permanent | Emergency disable of non-critical features under load | Keep permanently -- safety mechanism |
| Canary Flag | Medium | Hours to days | Route 1-5% of traffic to new code path | Remove after canary period passes |
| Trunk-Based Dev Flag | Low | Days | Hide incomplete code on main branch | Remove when feature is complete |
| Migration Flag | Medium | Weeks | Switch between old and new data path | Remove after migration verified |

[src1]

## Decision Tree

```
START
|-- Need to gradually roll out a new feature?
|   |-- YES --> Use a Release Toggle with percentage rollout (see Step 2)
|   +-- NO |
|-- Need to run an experiment (A/B test)?
|   |-- YES --> Use an Experiment Toggle with user segmentation
|   +-- NO |
|-- Need emergency disable capability?
|   |-- YES --> Use a Kill Switch (permanent, simple boolean)
|   +-- NO |
|-- Need to gate features by user plan/tier?
|   |-- YES --> Use a Permission Toggle (long-lived, user-attribute based)
|   +-- NO |
|-- Need to degrade non-critical services under load?
|   |-- YES --> Use an Ops Toggle (circuit breaker pattern)
|   +-- NO |
+-- DEFAULT --> Use a Release Toggle for trunk-based development
```

[src1]

## Step-by-Step Guide

### 1. Define a flag evaluation interface

Create a clean abstraction so your business logic does not depend on a specific flag provider. This lets you swap between custom implementations and managed services. [src2]

```typescript
// flag-service.ts -- minimal interface for flag evaluation
interface FlagService {
  isEnabled(flagKey: string, context?: FlagContext): boolean;
  getVariant(flagKey: string, context?: FlagContext): string;
}

interface FlagContext {
  userId?: string;
  email?: string;
  country?: string;
  plan?: string;
  percentageSeed?: number;  // stable hash for consistent bucketing
}
```

**Verify**: Ensure your interface supports boolean flags and multi-variant flags with user context.

### 2. Implement percentage rollout

Use consistent hashing so the same user always sees the same variant. Never use `Math.random()` -- it causes flickering between page loads. [src7]

```typescript
// percentage-rollout.ts
import { createHash } from 'crypto';

function isEnabledForPercentage(
  flagKey: string,
  userId: string,
  percentage: number  // 0-100
): boolean {
  // Stable hash: same user + flag always produces same bucket
  const hash = createHash('md5')
    .update(`${flagKey}:${userId}`)
    .digest('hex');
  const bucket = parseInt(hash.substring(0, 8), 16) % 100;
  return bucket < percentage;
}

// Usage: roll out to 10% of users
if (isEnabledForPercentage('new-checkout', user.id, 10)) {
  showNewCheckout();
} else {
  showOldCheckout();
}
```

**Verify**: Call `isEnabledForPercentage('flag', 'user-123', 50)` multiple times -- it must return the same value every time.

### 3. Add targeting rules

Layer targeting rules on top of percentage rollout for fine-grained control. Evaluate rules in priority order: explicit overrides first, then segments, then percentage. [src5]

```typescript
// targeting.ts
interface TargetingRule {
  attribute: string;       // e.g., "email", "country", "plan"
  operator: 'eq' | 'in' | 'contains' | 'startsWith';
  value: string | string[];
  enabled: boolean;
}

interface FlagConfig {
  key: string;
  enabled: boolean;           // global kill switch
  percentage: number;         // 0-100 for gradual rollout
  overrides: Record<string, boolean>;  // userId -> forced value
  rules: TargetingRule[];     // evaluated in order
}

function evaluateFlag(config: FlagConfig, ctx: FlagContext): boolean {
  if (!config.enabled) return false;
  // Check explicit user override first
  if (ctx.userId && ctx.userId in config.overrides) {
    return config.overrides[ctx.userId];
  }
  // Check targeting rules
  for (const rule of config.rules) {
    if (matchesRule(rule, ctx)) return rule.enabled;
  }
  // Fall back to percentage rollout
  if (ctx.userId) {
    return isEnabledForPercentage(config.key, ctx.userId, config.percentage);
  }
  return false;
}
```

**Verify**: Test with an overridden user (should always match), a rule-matched user, and a percentage-only user.

### 4. Implement flag cleanup automation

Set expiration dates at creation time and enforce them in CI. This is the most important step -- skipping it guarantees flag debt. [src4]

```typescript
// flag-registry.ts -- track flag lifecycle
interface FlagRegistration {
  key: string;
  type: 'release' | 'experiment' | 'ops' | 'permission' | 'kill_switch';
  owner: string;           // team or person responsible
  createdAt: Date;
  expiresAt: Date | null;  // null only for ops/kill_switch
  jiraTicket: string;      // cleanup ticket created at flag creation
}

// CI check: fail build if expired flags exist in code
function checkExpiredFlags(registry: FlagRegistration[]): string[] {
  const now = new Date();
  return registry
    .filter(f => f.expiresAt && f.expiresAt < now)
    .map(f => `EXPIRED FLAG: ${f.key} (owner: ${f.owner}, expired: ${f.expiresAt})`);
}
```

**Verify**: `checkExpiredFlags(registry)` should return an empty array in a healthy codebase.

## Code Examples

### Node.js: Custom Feature Flag Service with Redis

> Full script: [node-js-custom-feature-flag-service-with-redis.js](scripts/node-js-custom-feature-flag-service-with-redis.js) (36 lines)

```javascript
// Input:  Redis connection, flag configs stored as JSON
// Output: Boolean flag evaluation with percentage rollout
const Redis = require('ioredis');       // ioredis@5.x
const crypto = require('crypto');
class FeatureFlagService {
# ... (see full script)
```

### Python: Custom Feature Flag with Database Backend

> Full script: [python-custom-feature-flag-with-database-backend.py](scripts/python-custom-feature-flag-with-database-backend.py) (37 lines)

```python
# Input:  PostgreSQL connection, flag configs in flags table
# Output: Boolean flag evaluation with user targeting
import hashlib
from typing import Optional
import psycopg2  # psycopg2-binary==2.9.x
# ... (see full script)
```

### Go: Feature Flag Middleware for HTTP

> Full script: [go-feature-flag-middleware-for-http.go](scripts/go-feature-flag-middleware-for-http.go) (46 lines)

```go
// Input:  Flag config map, HTTP request with user context
// Output: HTTP middleware that injects flag values into request context
package featureflags
import (
	"crypto/md5"
# ... (see full script)
```

### Node.js: Unleash SDK Integration

> Full script: [node-js-unleash-sdk-integration.js](scripts/node-js-unleash-sdk-integration.js) (25 lines)

```javascript
// Input:  Unleash server URL, API token
// Output: Feature flag evaluation via managed service
const { initialize } = require('unleash-client');  // unleash-client@5.x
const unleash = initialize({
  url: 'https://unleash.example.com/api',
# ... (see full script)
```

## Anti-Patterns

### Wrong: Feature flag wrapping deeply nested code

```javascript
// BAD -- flag check buried inside business logic
function processOrder(order) {
  validateItems(order.items);
  calculateTax(order);
  if (featureFlags.isEnabled('new-shipping')) {  // buried deep
    applyNewShipping(order);
    if (featureFlags.isEnabled('new-tracking')) {  // nested flags!
      enableTracking(order);
    }
  } else {
    applyOldShipping(order);
  }
  chargePayment(order);
}
```

### Correct: Feature flag evaluated once at the boundary

```javascript
// GOOD -- evaluate at the entry point, pass result down
function processOrder(order, features) {
  validateItems(order.items);
  calculateTax(order);
  const shippingStrategy = features.useNewShipping
    ? newShippingStrategy
    : oldShippingStrategy;
  shippingStrategy.apply(order);
  chargePayment(order);
}

// Evaluate flags once at request boundary
app.post('/orders', (req, res) => {
  const features = {
    useNewShipping: flags.isEnabled('new-shipping', req.user.id),
  };
  processOrder(req.body, features);
});
```

[src1]

### Wrong: No expiration tracking on release flags

```javascript
// BAD -- flag created with no lifecycle management
await redis.set('flag:new-dashboard', JSON.stringify({
  enabled: true,
  percentage: 100,
  // No owner, no expiration, no cleanup ticket
  // This flag will live forever in the codebase
}));
```

### Correct: Flag with lifecycle metadata

```javascript
// GOOD -- every flag has an owner and expiration
await redis.set('flag:new-dashboard', JSON.stringify({
  enabled: true,
  percentage: 100,
  owner: 'frontend-team',
  createdAt: '2026-02-01',
  expiresAt: '2026-03-15',
  jiraTicket: 'FE-4521',
  type: 'release',
}));
// CI pipeline checks for expired flags and fails build
```

[src4]

### Wrong: Using Math.random() for percentage rollout

```python
# BAD -- random gives different results on every evaluation
import random

def is_enabled(flag_key, user_id, percentage):
    return random.random() * 100 < percentage  # flickers every request!
```

### Correct: Stable hash-based bucketing

```python
# GOOD -- same user always gets same bucket
import hashlib

def is_enabled(flag_key, user_id, percentage):
    h = hashlib.md5(f"{flag_key}:{user_id}".encode()).hexdigest()
    bucket = int(h[:8], 16) % 100
    return bucket < percentage  # deterministic per user
```

[src7]

### Wrong: Flag dependencies without explicit ordering

```javascript
// BAD -- flag B depends on flag A, but no explicit relationship
if (flags.isEnabled('new-api') && flags.isEnabled('new-api-v2')) {
  // What if new-api is off but new-api-v2 is on?
  // Undefined behavior, hard to debug
}
```

### Correct: Explicit flag dependency declaration

```javascript
// GOOD -- declare dependencies in flag config
const flagConfig = {
  'new-api-v2': {
    enabled: true,
    dependsOn: ['new-api'],  // must be enabled first
    percentage: 50,
  },
};

function evaluateWithDeps(flagKey, userId) {
  const config = flagConfig[flagKey];
  if (config.dependsOn) {
    const depsOk = config.dependsOn.every(
      dep => evaluateWithDeps(dep, userId)
    );
    if (!depsOk) return false;
  }
  return isEnabledForPercentage(flagKey, userId, config.percentage);
}
```

[src2]

## Common Pitfalls

- **Flag flickering**: Using non-deterministic randomness for percentage rollout causes users to see features appear and disappear between requests. Fix: use stable hashing with `md5(flagKey + userId)` for consistent bucketing. [src7]
- **Stale flag accumulation**: Teams create flags but never clean them up, leading to thousands of dead code paths. Fix: set expiration dates at creation, fail CI on expired flags, dedicate quarterly sprints to flag cleanup. [src4]
- **Testing only the happy path**: Only testing the flag-on path means the flag-off path rots silently. Fix: run CI with `ALL_FLAGS=on` and `ALL_FLAGS=off` matrix to catch both paths. [src2]
- **Flag evaluation in hot loops**: Evaluating flags inside tight loops (per-item in a list) when the value is the same for all items. Fix: evaluate once before the loop and store the result in a variable. [src6]
- **No centralized flag dashboard**: Teams manage flags in scattered config files, losing track of what flags exist. Fix: use a centralized flag service or registry -- even a database table is better than scattered YAML files. [src3]
- **Coupling deployment and flag state**: Deploying new code and enabling the flag simultaneously defeats the purpose. Fix: deploy first with the flag off, verify the deployment is stable, then enable the flag separately. [src5]

## Diagnostic Commands

```bash
# List all feature flags in a Redis-backed system
redis-cli KEYS "flag:*" | while read key; do echo "$key: $(redis-cli GET $key)"; done

# Find all flag references in codebase (grep for cleanup)
grep -rn "isEnabled\|is_enabled\|IsEnabled\|feature_flag\|featureFlag" --include="*.ts" --include="*.py" --include="*.go" src/

# Count flags by type in a JSON config file
cat flags.json | python3 -c "import json,sys,collections; d=json.load(sys.stdin); print(dict(collections.Counter(f.get('type','unknown') for f in d.values())))"

# Check for expired flags (flags with expiresAt before today)
cat flags.json | python3 -c "
import json, sys
from datetime import datetime
flags = json.load(sys.stdin)
now = datetime.now().isoformat()[:10]
for k, v in flags.items():
    exp = v.get('expiresAt')
    if exp and exp < now:
        print(f'EXPIRED: {k} (expired {exp}, owner: {v.get(\"owner\", \"unknown\")})')
"
```

## Version History & Compatibility

| Technology | Status | Key Feature | Notes |
|---|---|---|---|
| OpenFeature v0.7 | Current spec (2024) | Vendor-neutral SDK interface | Adopt for new projects to avoid vendor lock-in |
| LaunchDarkly SDK v8 | Current | Streaming updates, local evaluation | Industry leader, $$$. Free tier: 1K MAU |
| Unleash v5 | Current | Self-hosted OSS, Prometheus metrics | Best open-source option. Pro tier for RBAC |
| Flagsmith v2 | Current | Open-source, edge evaluation | Good for self-hosted with edge requirements |
| GrowthBook v2 | Current | Statistics engine built-in | Best when A/B testing is primary use case |
| Custom (DB/Redis) | Evergreen | Full control, no vendor dependency | Good for <50 flags, becomes painful at scale |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Gradually rolling out features to reduce blast radius | Toggling between dev/staging/prod environments | Environment variables or config management |
| Running A/B experiments on UI or backend behavior | Permanently gating features by user role/plan | RBAC or entitlement system |
| Need emergency kill switch for non-critical services | Configuring infrastructure (ports, URLs, timeouts) | Infrastructure-as-code (Terraform, Helm) |
| Hiding incomplete features on main branch (trunk-based dev) | Simple on/off that never changes after deploy | Hard-code or compile-time flag |
| Canary releases -- test with 1% traffic before full rollout | Fewer than 3 developers or no CI/CD pipeline | Ship directly, iterate fast |
| Decoupling deploy from release in regulated environments | Feature is security-critical (auth, encryption) | Code review + staged deployment instead |

## Important Caveats

- Feature flag evaluation adds latency. For managed services with streaming SDKs (LaunchDarkly, Unleash), local evaluation takes <1ms. For remote API calls, expect 20-100ms. Cache aggressively.
- Flag combinatorial explosion: with N boolean flags, you have 2^N possible code paths. At 20 flags, that is over 1 million paths. Keep active flag count under 50 and test critical combinations.
- Client-side flags (browser/mobile) can be inspected and manipulated by users. Never use client-side evaluation for security-sensitive gates -- always verify server-side.
- Percentage rollout is only consistent if you use the same hashing algorithm and seed everywhere. If your Node.js and Python services hash differently, a user might see the feature in one service but not another.
- Uber built Piranha, an automated tool to remove stale feature flags. Consider building similar automation if you have >100 flags.

## Related Units

- [API Versioning Strategies](/software/patterns/api-versioning/2026)
- [Database Migration Strategies](/software/patterns/database-migration-strategies/2026)
- [State Machine Implementation](/software/patterns/state-machine-implementation/2026)
- [Role-Based Access Control](/software/patterns/rbac-implementation/2026)
