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# 1 2 3 4 5 6 . . . . . . C D E E P R r e n n e e e p a a r m a l b b c o t o l l e v e y e e n e t f c f f a f l o o o g l a d r r e a g e g d b r ( b e e o a d e v t l n i h e a l d s i l o a n o u u o b d p s t l l e e d e f r r ( d l s s 1 c ) a % o g d → e 1 0 % → 5 0 % → 1 0 0 % )
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# # e s u # f n t n h s l e e G a o e B a w s o b w _ a g _ t o l _ s d 1 t d e b t : h : _ e r i n t i v n d e a p a g e w _ e g s _ d _ u c c a v e r h s 2 , i e h _ p c b a c t k o p o i o a i n v u r f e t d u , _ s f i a l n c o g t w i o n - o r i e n t e d
Include ticket numbers for traceability:
P R O J - 1 2 3 4 _ n e w _ c h e c k o u t _ f l o w
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, expensiveUnleash - Open source, self-hostedFlagsmith - Open source with cloud optionConfigCat - Simple and affordablePostHog - Feature flags + analyticsThey 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.