Deployment and release are not the same thing. Feature flags let you deploy code to production without releasing it to users. Here’s how to implement them without creating technical debt.

Why Feature Flags?

WDWDieietptphlhloooPuyfyrtlo=abfgllRsTeae:emglssset?:asiIenns=ptraRonidtskdiGsraabdlueal(nroolrloolultbackFunleledreedl)ease

Use cases:

  • Kill switches: Disable broken features instantly
  • Gradual rollouts: 1% → 10% → 50% → 100%
  • A/B testing: Compare feature variants
  • Beta access: Enable for specific users
  • Ops toggles: Turn off expensive features under load

Simple Implementation

In-Code Flags

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# config.py
FEATURES = {
    "new_checkout": False,
    "dark_mode": True,
    "ai_recommendations": False,
}

# Usage
from config import FEATURES

def checkout():
    if FEATURES["new_checkout"]:
        return new_checkout_flow()
    return legacy_checkout_flow()

Problem: Requires redeploy to change flags.

Environment Variables

1
2
3
4
5
6
7
8
import os

def is_enabled(flag_name: str) -> bool:
    return os.getenv(f"FEATURE_{flag_name.upper()}", "false").lower() == "true"

# Usage
if is_enabled("new_checkout"):
    return new_checkout_flow()
1
2
# Enable via environment
FEATURE_NEW_CHECKOUT=true python app.py

Problem: Still requires restart to change.

Database-Backed Flags

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# models.py
class FeatureFlag(Base):
    __tablename__ = "feature_flags"
    
    name = Column(String, primary_key=True)
    enabled = Column(Boolean, default=False)
    rollout_percentage = Column(Integer, default=0)
    enabled_for_users = Column(ARRAY(String), default=[])
    
# flags.py
from functools import lru_cache
import time

_cache = {}
_cache_ttl = 60  # seconds

def get_flag(name: str) -> FeatureFlag:
    now = time.time()
    if name in _cache and _cache[name]['expires'] > now:
        return _cache[name]['flag']
    
    flag = db.query(FeatureFlag).filter_by(name=name).first()
    _cache[name] = {'flag': flag, 'expires': now + _cache_ttl}
    return flag

def is_enabled(name: str, user_id: str = None) -> bool:
    flag = get_flag(name)
    if not flag:
        return False
    
    # Check user allowlist
    if user_id and user_id in (flag.enabled_for_users or []):
        return True
    
    # Check percentage rollout
    if flag.rollout_percentage > 0:
        # Consistent hashing so user always gets same result
        hash_val = hash(f"{name}:{user_id}") % 100
        return hash_val < flag.rollout_percentage
    
    return flag.enabled

Feature Flag Service

LaunchDarkly-Style SDK

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from launchdarkly_client import LDClient

client = LDClient("sdk-key-xxx")

# Basic check
if client.variation("new-checkout", user, False):
    return new_checkout_flow()

# With user context
user = {
    "key": "user-123",
    "email": "alice@example.com",
    "custom": {
        "plan": "premium",
        "country": "US"
    }
}

# Targeting rules can use these attributes
show_feature = client.variation("premium-feature", user, False)

Self-Hosted: Unleash

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# docker-compose.yml
services:
  unleash:
    image: unleashorg/unleash-server:latest
    environment:
      - DATABASE_URL=postgres://unleash:password@db/unleash
    ports:
      - "4242:4242"
    depends_on:
      - db
  
  db:
    image: postgres:15
    environment:
      - POSTGRES_USER=unleash
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=unleash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from UnleashClient import UnleashClient

client = UnleashClient(
    url="http://unleash:4242/api",
    app_name="my-app",
    custom_headers={"Authorization": "token"}
)
client.initialize_client()

# Usage
if client.is_enabled("new-checkout"):
    return new_checkout_flow()

# With context
context = {"userId": "user-123", "properties": {"plan": "premium"}}
if client.is_enabled("premium-feature", context):
    return premium_flow()

Rollout Strategies

Percentage Rollout

1
2
3
4
5
6
def percentage_rollout(flag_name: str, user_id: str, percentage: int) -> bool:
    """Consistent percentage rollout using hash."""
    # Same user always gets same result for same flag
    hash_input = f"{flag_name}:{user_id}"
    hash_value = int(hashlib.md5(hash_input.encode()).hexdigest(), 16) % 100
    return hash_value < percentage
DDDDDaaaaayyyyy12345:::::15251%%500%%0%MSWAFotalunitmlilcoltlhsotrrgmeoetleotherdrear?irsoceerCsso,ntliantueency

User Targeting

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def should_enable(flag: FeatureFlag, user: User) -> bool:
    # Explicit allowlist
    if user.id in flag.allowed_users:
        return True
    
    # Group targeting
    if user.is_beta_tester and "beta" in flag.target_groups:
        return True
    
    # Attribute rules
    for rule in flag.rules:
        if evaluate_rule(rule, user):
            return True
    
    # Default to percentage rollout
    return percentage_rollout(flag.name, user.id, flag.percentage)

Gradual Rollout with Metrics

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import statsd

stats = statsd.StatsClient()

def checkout():
    flag_enabled = is_enabled("new_checkout", current_user.id)
    
    # Track which variant
    stats.incr(f"checkout.variant.{'new' if flag_enabled else 'old'}")
    
    start = time.time()
    try:
        if flag_enabled:
            result = new_checkout_flow()
        else:
            result = legacy_checkout_flow()
        
        # Track success
        stats.incr(f"checkout.success.{'new' if flag_enabled else 'old'}")
        return result
    
    except Exception as e:
        # Track failure
        stats.incr(f"checkout.error.{'new' if flag_enabled else 'old'}")
        raise
    finally:
        # Track latency
        duration = time.time() - start
        stats.timing(f"checkout.duration.{'new' if flag_enabled else 'old'}", duration)

Kill Switches

Circuit Breaker Pattern

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class KillSwitch:
    def __init__(self, flag_name: str):
        self.flag_name = flag_name
        self.error_count = 0
        self.last_check = 0
        self.auto_disabled = False
    
    def is_enabled(self) -> bool:
        # Check remote flag
        if not get_flag(self.flag_name).enabled:
            return False
        
        # Check auto-disable
        if self.auto_disabled:
            return False
        
        return True
    
    def record_error(self):
        self.error_count += 1
        if self.error_count > 10:  # Threshold
            self.auto_disabled = True
            alert(f"Kill switch activated for {self.flag_name}")
    
    def record_success(self):
        self.error_count = max(0, self.error_count - 1)

# Usage
new_search = KillSwitch("new_search")

def search(query):
    if not new_search.is_enabled():
        return legacy_search(query)
    
    try:
        result = new_search_engine(query)
        new_search.record_success()
        return result
    except Exception as e:
        new_search.record_error()
        return legacy_search(query)  # Fallback

Frontend Flags

React Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// FlagProvider.jsx
import { createContext, useContext, useEffect, useState } from 'react';

const FlagContext = createContext({});

export function FlagProvider({ children }) {
  const [flags, setFlags] = useState({});
  
  useEffect(() => {
    fetch('/api/flags')
      .then(res => res.json())
      .then(setFlags);
    
    // Poll for updates
    const interval = setInterval(() => {
      fetch('/api/flags').then(res => res.json()).then(setFlags);
    }, 30000);
    
    return () => clearInterval(interval);
  }, []);
  
  return (
    <FlagContext.Provider value={flags}>
      {children}
    </FlagContext.Provider>
  );
}

export function useFlag(name, defaultValue = false) {
  const flags = useContext(FlagContext);
  return flags[name] ?? defaultValue;
}

// Usage
function CheckoutButton() {
  const newCheckout = useFlag('new-checkout');
  
  if (newCheckout) {
    return <NewCheckoutButton />;
  }
  return <LegacyCheckoutButton />;
}

Server-Side Rendering

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// pages/checkout.js (Next.js)
export async function getServerSideProps({ req }) {
  const flags = await getFlags(req.cookies.userId);
  
  return {
    props: {
      flags,
    },
  };
}

function CheckoutPage({ flags }) {
  return (
    <FlagProvider initialFlags={flags}>
      <Checkout />
    </FlagProvider>
  );
}

Flag Lifecycle

123456......CDTRRCREEOELEVSLLEAETLEATLOANEOUSUPTEPFEEG1Rlnnr0eaaaa0mgbbd%olluvceeaeerlneffafaoopbltrrelaeregddQcd,eAefv,nrdetoilbamsoegaptecbeaolridestnedecosrnteleaDyrsOseN'TSKIPTHIS

Technical Debt: Flag Cleanup

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Bad: Flags accumulate forever
if is_enabled("feature_q1_2023"):  # Still here in 2026?
    ...

# Good: Include expiration
class FeatureFlag:
    expires_at: datetime  # Alert if flag still exists after this
    
# CI check
def check_expired_flags():
    expired = db.query(FeatureFlag).filter(
        FeatureFlag.expires_at < datetime.now()
    ).all()
    
    if expired:
        raise Exception(f"Expired flags need cleanup: {expired}")

Testing with Flags

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# test_checkout.py
import pytest

@pytest.fixture
def enable_new_checkout(monkeypatch):
    monkeypatch.setenv("FEATURE_NEW_CHECKOUT", "true")

@pytest.fixture  
def disable_new_checkout(monkeypatch):
    monkeypatch.setenv("FEATURE_NEW_CHECKOUT", "false")

def test_new_checkout_flow(enable_new_checkout):
    result = checkout()
    assert result.template == "new_checkout.html"

def test_legacy_checkout_flow(disable_new_checkout):
    result = checkout()
    assert result.template == "legacy_checkout.html"

# Test both paths always
@pytest.mark.parametrize("flag_enabled", [True, False])
def test_checkout_completes(flag_enabled, monkeypatch):
    monkeypatch.setenv("FEATURE_NEW_CHECKOUT", str(flag_enabled).lower())
    result = checkout()
    assert result.status == "success"

The Checklist

  • Flags stored externally (not hardcoded)
  • Consistent user bucketing (same user = same experience)
  • Kill switch capability
  • Metrics per variant
  • Expiration dates on flags
  • Cleanup process defined
  • Tests cover both paths

Start Here

  1. Today: Add one kill switch for your riskiest feature
  2. This week: Implement database-backed flags
  3. This month: Add percentage rollout capability
  4. This quarter: Automate flag cleanup in CI

The goal: deploy anytime, release when ready, rollback instantly.


Feature flags turn deployments from events into non-events. And that’s exactly what you want.