There’s a subtle but powerful distinction in modern software delivery: deployment is not release.

Deployment means your code is running in production. Release means your users can see it. Feature flags are the bridge between these two concepts—and mastering them changes how you think about shipping software.

The Problem with Traditional Deployment

In the old model, deploying code meant releasing features:

1
2
3
# Old way: deploy = release
git push origin main
# Boom, everyone sees the new feature immediately

This creates pressure. You can’t deploy partially-complete work. You can’t test in production with real traffic. And if something breaks, your only option is another deploy to roll back.

Feature flags flip this model:

1
2
3
4
5
# New way: deploy != release
git push origin main
# Code is live, but flag is OFF
# Enable for 1% of users, watch metrics
# Gradually roll out to 100%

Implementing Feature Flags

At its simplest, a feature flag is a conditional:

1
2
3
4
5
# Basic feature flag
if feature_flags.is_enabled("new_checkout_flow", user_id=user.id):
    return new_checkout()
else:
    return legacy_checkout()

But production systems need more sophistication. Here’s a pattern I use with environment-based configuration:

 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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# config/feature_flags.py
import os
from dataclasses import dataclass
from typing import Optional
import hashlib

@dataclass
class FeatureFlag:
    name: str
    enabled: bool = False
    rollout_percentage: int = 0
    allowed_users: list[str] = None
    
    def is_enabled_for(self, user_id: Optional[str] = None) -> bool:
        # Always check kill switch first
        if not self.enabled:
            return False
        
        # Check allowlist
        if self.allowed_users and user_id in self.allowed_users:
            return True
        
        # Percentage rollout using consistent hashing
        if self.rollout_percentage > 0 and user_id:
            hash_val = int(hashlib.md5(
                f"{self.name}:{user_id}".encode()
            ).hexdigest(), 16)
            return (hash_val % 100) < self.rollout_percentage
        
        return self.rollout_percentage == 100

class FeatureFlagService:
    def __init__(self):
        self.flags = self._load_flags()
    
    def _load_flags(self) -> dict[str, FeatureFlag]:
        # In production, load from database/config service
        return {
            "new_checkout": FeatureFlag(
                name="new_checkout",
                enabled=os.getenv("FF_NEW_CHECKOUT", "false") == "true",
                rollout_percentage=int(os.getenv("FF_NEW_CHECKOUT_PCT", "0")),
            ),
            "dark_mode": FeatureFlag(
                name="dark_mode",
                enabled=True,
                rollout_percentage=100,
            ),
        }
    
    def is_enabled(self, flag_name: str, user_id: str = None) -> bool:
        flag = self.flags.get(flag_name)
        if not flag:
            return False
        return flag.is_enabled_for(user_id)

# Usage
flags = FeatureFlagService()

@app.route("/checkout")
def checkout():
    if flags.is_enabled("new_checkout", user_id=current_user.id):
        return render_template("checkout_v2.html")
    return render_template("checkout.html")

The Consistent Hashing Trick

Notice the MD5 hashing in the rollout logic. This ensures:

  1. Consistency: The same user always gets the same experience
  2. Stickiness: Users don’t flip-flop between variants
  3. Determinism: No database state needed per-user
1
2
3
4
5
6
# User "alice" at 25% rollout:
hash("new_checkout:alice") % 100 = 42  # > 25, sees OLD
hash("new_checkout:alice") % 100 = 42  # Still 42, still OLD

# Increase to 50% rollout:
hash("new_checkout:alice") % 100 = 42  # < 50, now sees NEW

The user’s experience changes based on rollout percentage, not random chance per request.

Infrastructure for Feature Flags

For serious deployments, use a dedicated feature flag service. Here’s a Docker Compose setup with Unleash (open-source):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# docker-compose.yml
version: "3.8"
services:
  unleash:
    image: unleashorg/unleash-server:latest
    environment:
      DATABASE_URL: postgres://unleash:password@db:5432/unleash
      DATABASE_SSL: "false"
    depends_on:
      - db
    ports:
      - "4242:4242"

  db:
    image: postgres:15
    environment:
      POSTGRES_USER: unleash
      POSTGRES_PASSWORD: password
      POSTGRES_DB: unleash
    volumes:
      - unleash_data:/var/lib/postgresql/data

volumes:
  unleash_data:

Then integrate with your application:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Using Unleash client
from UnleashClient import UnleashClient

client = UnleashClient(
    url="http://localhost:4242/api",
    app_name="my-app",
    custom_headers={"Authorization": os.getenv("UNLEASH_API_KEY")}
)
client.initialize_client()

@app.route("/api/dashboard")
def dashboard():
    context = {"userId": current_user.id}
    
    if client.is_enabled("new_dashboard", context):
        return jsonify(get_new_dashboard_data())
    return jsonify(get_legacy_dashboard_data())

Terraform for Flag Infrastructure

If you’re using LaunchDarkly or similar SaaS, you can manage flags as code:

 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
43
44
45
46
# terraform/feature_flags.tf
resource "launchdarkly_feature_flag" "new_checkout" {
  project_key = "my-project"
  key         = "new-checkout"
  name        = "New Checkout Flow"
  description = "Redesigned checkout with one-click purchase"
  
  variation_type = "boolean"
  
  variations {
    value = true
    name  = "Enabled"
  }
  
  variations {
    value = false
    name  = "Disabled"
  }
  
  defaults {
    on_variation  = 0  # true
    off_variation = 1  # false
  }
  
  tags = ["checkout", "q1-2026"]
}

resource "launchdarkly_feature_flag_environment" "new_checkout_prod" {
  flag_id = launchdarkly_feature_flag.new_checkout.id
  env_key = "production"
  
  on = true
  
  rules {
    clauses {
      attribute = "email"
      op        = "endsWith"
      values    = ["@company.com"]
    }
    variation = 0  # Internal users see new version
  }
  
  fallthrough {
    rollout_weights = [10, 90]  # 10% new, 90% old
  }
}

Best Practices

1. Flag Naming Convention

fE--exaaffotmeepupaasrltt_eeuuc_srri<:eert__cecsuaheimeat>cr__kcb<ohrfu_eetaaa_ikto_eunsrreu_ecgp>lga_iey<csmvkteabinruotiynsa_sne_tnv>a2bled

2. Flag Lifecycle Management

Every flag should have an expiration plan:

1
2
3
4
5
6
7
8
@dataclass
class FeatureFlag:
    name: str
    enabled: bool
    created_at: datetime
    expires_at: datetime  # When to remove the flag
    owner: str            # Who's responsible for cleanup
    jira_ticket: str      # Tracking removal

3. Flag Categories

  • Release flags: Temporary, for gradual rollout
  • Ops flags: Circuit breakers, kill switches
  • Experiment flags: A/B tests with metrics
  • Permission flags: Entitlements, premium features

4. Monitoring Flag Impact

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Log flag decisions for analytics
import structlog
logger = structlog.get_logger()

def is_enabled(flag_name: str, user_id: str) -> bool:
    result = flags.is_enabled(flag_name, user_id)
    logger.info(
        "feature_flag_evaluated",
        flag=flag_name,
        user_id=user_id,
        result=result,
        rollout_pct=flags.get(flag_name).rollout_percentage
    )
    return result

The Rollback Superpower

The killer feature of feature flags: instant rollback without deployment.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Something's wrong with new checkout!

# Old way:
git revert abc123
git push
# Wait for CI/CD pipeline...
# Wait for deployment...
# 15-30 minutes of pain

# With feature flags:
curl -X PATCH https://api.launchdarkly.com/flags/new-checkout \
  -H "Authorization: $LD_API_KEY" \
  -d '{"on": false}'
# Done in seconds

Conclusion

Feature flags transform deployment from a binary event into a gradual, controllable process. They let you:

  • Deploy incomplete features safely
  • Test in production with real traffic
  • Roll back instantly without redeploying
  • Run experiments and measure impact
  • Separate engineering decisions from business decisions

The overhead of maintaining flag infrastructure pays for itself the first time you catch a bug affecting 1% of users instead of 100%.

Start simple—even a config file with booleans is better than nothing. Then evolve toward proper flag management as your needs grow.

Your deploy pipeline should be boring. Feature flags make it possible.