Feature flags are simple in concept, tricky in practice. Here's how to use them without creating technical debt landmines.
March 5, 2026 · 6 min · 1247 words · Rob Washington
Table of Contents
Feature flags seem simple: wrap code in an if statement, flip a boolean, ship whenever you want. In practice, they’re one of the fastest ways to accumulate invisible technical debt. Here’s how to get the benefits without the baggage.
The core value proposition is decoupling deployment from release:
1
2
3
4
5
6
7
8
9
10
11
12
# Without flags: deploy = releasedefcheckout():process_payment()send_confirmation()# With flags: deploy when ready, release when confidentdefcheckout():process_payment()iffeature_enabled("new_confirmation_flow"):send_rich_confirmation()else:send_confirmation()
# Scattered flags everywheredefsome_function():ifconfig.get("new_thing"):# 50 lines of new codeelse:# 50 lines of old codedefanother_function():ifconfig.get("new_thing"):# different behavior
classCheckoutStrategy:defprocess(self,cart):...classLegacyCheckout(CheckoutStrategy):defprocess(self,cart):# old implementationclassNewCheckout(CheckoutStrategy):defprocess(self,cart):# new implementationdefget_checkout_strategy(user)->CheckoutStrategy:iffeature_enabled("checkout_v2",user):returnNewCheckout()returnLegacyCheckout()
Benefits: Flag check in one place, implementations isolated, easy to test, trivial to remove.
deffeature_enabled(flag:str,context:dict=None)->bool:"""
Context can include:
- user_id: for percentage rollouts
- user_attributes: for targeting
- environment: for env-specific behavior
"""flag_config=get_flag_config(flag)# Check kill switch firstifflag_config.get("force_off"):returnFalseifflag_config.get("force_on"):returnTrue# Percentage rollout based on userifcontextand"user_id"incontext:bucket=hash(context["user_id"])%100returnbucket<flag_config.get("rollout_percentage",0)returnflag_config.get("default",False)
Consistent bucketing matters—users shouldn’t flip between experiences.
Feature flags multiply test scenarios. Be strategic:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Test the flag evaluation logicdeftest_rollout_percentage():assertfeature_enabled("test",{"user_id":"a"})==True# bucket 23assertfeature_enabled("test",{"user_id":"b"})==False# bucket 67# Test each code path independentlydeftest_legacy_checkout():withflag_override("checkout_v2",False):result=checkout(cart)assertresult.uses_legacy_flowdeftest_new_checkout():withflag_override("checkout_v2",True):result=checkout(cart)assertresult.uses_new_flow# Don't test the combinatorial explosion# Focus on: flag off, flag on, rollout logic
Feature flags are a liability, not an asset. Every flag is:
Code that might run
Code that might not run
A test you need to write
A decision someone needs to make
Debt that accrues interest
Use them when the value exceeds the cost. Remove them the moment they’ve served their purpose. Treat flag count like you treat dependency count—lower is better.
The best feature flag is the one you already removed.
📬 Get the Newsletter
Weekly insights on DevOps, automation, and CLI mastery. No spam, unsubscribe anytime.