Separating deployment from release is one of the best things you can do for your team’s sanity. Feature flags make this possible.

The Core Idea

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Without flags: deploy = release
def checkout():
    process_payment()
    send_confirmation()

# With flags: deploy != release
def checkout():
    process_payment()
    if feature_enabled("new_confirmation_email"):
        send_new_confirmation()  # Deployed but not released
    else:
        send_confirmation()

Code ships to production. Flag decides if users see it.

Simple Implementation

 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
import os
import json

class FeatureFlags:
    def __init__(self):
        self.flags = json.loads(os.environ.get("FEATURE_FLAGS", "{}"))
    
    def is_enabled(self, flag: str, user_id: str = None) -> bool:
        config = self.flags.get(flag, {})
        
        if not config.get("enabled", False):
            return False
        
        # Percentage rollout
        if "percentage" in config and user_id:
            return hash(f"{flag}:{user_id}") % 100 < config["percentage"]
        
        # User allowlist
        if "users" in config:
            return user_id in config["users"]
        
        return True

flags = FeatureFlags()

# Usage
if flags.is_enabled("dark_mode", user_id=current_user.id):
    render_dark_theme()

Environment variable:

1
2
3
4
5
{
  "dark_mode": {"enabled": true, "percentage": 10},
  "new_checkout": {"enabled": true, "users": ["alice", "bob"]},
  "beta_api": {"enabled": false}
}

Rollout Strategies

1. Boolean (Kill Switch)

1
2
if flags.is_enabled("maintenance_mode"):
    return "Site under maintenance"

On or off. Good for emergencies.

2. Percentage Rollout

1
2
# Start at 1%, increase to 5%, 25%, 50%, 100%
{"new_feature": {"enabled": true, "percentage": 5}}

Gradually expose users. Monitor errors at each step.

3. User Targeting

1
2
3
4
5
# Internal testing first
{"new_feature": {"enabled": true, "users": ["internal@company.com"]}}

# Then beta users
{"new_feature": {"enabled": true, "groups": ["beta_testers"]}}

4. Environment-Based

1
{"debug_panel": {"enabled": true, "environments": ["staging", "development"]}}

Never reaches production.

The Lifecycle

123456......CDEEPRrenneeepaarmalbbcotolleveyeenetfcffaflooogladrreagegdbr(beeoadevtlnihealdsiloanouuobdpstlleedefrr(dlss1c)a%ogde10%50%100%)

Step 6 is crucial. Flags are temporary. Old flags become tech debt.

When to Use Flags

Good uses:

  • Gradual rollouts
  • A/B testing
  • Kill switches for risky features
  • Beta programs
  • Operational toggles (maintenance mode)

Bad uses:

  • Permanent configuration (use config instead)
  • Authorization (use permissions)
  • Environment differences (use env vars)

Flag Naming Convention

#esu#fntnhsleeGaoeBawsobw_ag_tol_sd1tdebt:h:_erintivndeapagew_egs_d_uccaverhs2,ieh_pcbactkopoioainvurfetdu,_sfialncogtwion-oriented

Include ticket numbers for traceability:

PROJ-1234_new_checkout_flow

Avoiding Flag Spaghetti

1
2
3
4
5
6
7
8
9
# Bad: nested flag logic
if flags.is_enabled("feature_a"):
    if flags.is_enabled("feature_b"):
        if flags.is_enabled("feature_c"):
            # What even is this state?

# Better: combine related flags
if flags.is_enabled("checkout_v2"):  # One flag, one experience
    use_new_checkout()

Testing with Flags

1
2
3
4
5
6
7
8
9
def test_new_feature_enabled():
    with override_flag("new_feature", True):
        result = my_function()
        assert result == "new behavior"

def test_new_feature_disabled():
    with override_flag("new_feature", False):
        result = my_function()
        assert result == "old behavior"

Test both paths. The old path still matters until the flag is removed.

Production Services

For serious use, consider:

  • LaunchDarkly - Full-featured, expensive
  • Unleash - Open source, self-hosted
  • Flagsmith - Open source with cloud option
  • ConfigCat - Simple and affordable
  • PostHog - Feature flags + analytics

They provide:

  • UI for non-engineers
  • Audit logs
  • Targeting rules
  • Analytics integration

The Cleanup Problem

1
2
3
4
5
6
7
8
# Set a reminder when creating the flag
class Flag:
    def __init__(self, name: str, cleanup_by: date):
        self.name = name
        self.cleanup_by = cleanup_by
        
        if date.today() > cleanup_by:
            log.warning(f"Flag {name} is past cleanup date!")

Track flags in a spreadsheet, ticket, or dedicated tool. Review monthly.

Rollback in 30 Seconds

1
2
3
4
5
6
# Something's wrong with new checkout
curl -X PATCH https://api.example.com/flags/new_checkout \
  -d '{"enabled": false}'

# Users immediately see old behavior
# No deploy required

This is the killer feature. Instant rollback without code changes.

The Philosophy

Feature flags shift risk from deployment to release. You can:

  • Deploy Friday afternoon (code is hidden)
  • Release Monday morning (flip the flag)
  • Rollback in seconds if needed

Deployment becomes routine. Release becomes a business decision.

That separation is worth the overhead of managing flags.