| name | feature-flags |
| description | Design and implement a feature flag system for safe, gradual feature rollouts. Covers flag types, targeting rules, lifecycle management, technical implementation, and cleanup processes. |
| argument-hint | ["tech stack","rollout strategy","user targeting needs","flag management tool"] |
| allowed-tools | Read, Write, Bash |
Feature Flag System
Feature flags (also called feature toggles) decouple code deployment from feature release. They enable dark launches, gradual rollouts, A/B testing, kill switches, and ops toggles — without deploying new code. Done well, they are a superpower. Done poorly, they become a maintenance nightmare.
Flag Types
| Type | Purpose | Lifetime | Example |
|---|
| Release flag | Progressive rollout of new feature | Short (days–weeks) | New checkout flow for 10% of users |
| Experiment flag | A/B test | Short (test duration) | Button color test |
| Ops/kill switch | Disable feature without deploy | Medium | Disable recommendations under load |
| Permission flag | Feature available to specific users | Long | Beta feature for paid plan only |
| Infrastructure flag | Switch between implementations | Medium | New payment processor |
Rollout Strategies
1. Internal only → Engineers, QA team, internal accounts
2. Alpha (1–5%) → Opt-in early adopters or random segment
3. Beta (10–25%) → Broader rollout; monitor metrics
4. General (50%) → Half of users; compare to control
5. Full (100%) → All users; flag can be removed
6. Kill switch → Instant rollback to 0% without deploy
Process
- Name the flag — use a descriptive, scoped name:
[team]_[feature]_[detail] e.g. checkout_new_payment_ui_v2.
- Choose the flag type — release, experiment, ops, permission.
- Define targeting rules — who gets the feature? Random %, user IDs, plan type, geography.
- Set the default — what does a user get if the flag system is unavailable? Default to safe state.
- Implement the flag check — wrap the feature code, not the business logic.
- Test both paths — always test flag on AND flag off before deploying.
- Define rollout stages and success metrics — what metric validates each stage?
- Roll out incrementally — 1% → 10% → 50% → 100%, monitoring at each stage.
- Schedule cleanup — add a ticket to remove the flag when at 100% for 2+ weeks.
- Document flag lifecycle — who owns it, when it was created, when it expires.
Implementation Examples
LaunchDarkly (Managed Service)
import ldclient
from ldclient.config import Config
ldclient.set_config(Config("sdk-key-XXXXXXXX"))
client = ldclient.get()
def show_new_checkout(user_id: str, user_plan: str) -> bool:
context = {
"kind": "user",
"key": user_id,
"plan": user_plan,
"custom": {"region": "us-east"}
}
return client.variation("checkout-new-ui", context, False)
if show_new_checkout(request.user.id, request.user.plan):
return render_new_checkout()
else:
return render_legacy_checkout()
Self-Hosted (Redis + Python)
import redis
import hashlib
r = redis.Redis(host='redis', port=6379, db=0)
class FeatureFlags:
def __init__(self):
self.redis = r
self._cache = {}
def is_enabled(self, flag_name: str, user_id: str = None) -> bool:
try:
flag = self._get_flag(flag_name)
if not flag:
return False
if flag['status'] == 'off':
return False
if flag['status'] == 'on':
return True
if flag['status'] == 'rollout':
return self._in_rollout(flag_name, user_id, flag['percentage'])
except Exception:
return False
def _get_flag(self, name: ) -> :
raw = .redis.hgetall()
raw:
{k.decode(): v.decode() k, v raw.items()}
() -> :
hash_val = (hashlib.md5(.encode()).hexdigest(), )
(hash_val % ) < (pct)
flags = FeatureFlags()
r.hset(, mapping={
: ,
: ,
: ,
: ,
: ,
})
React Client-Side Flag
import { useFlags } from 'launchdarkly-react-client-sdk';
function CheckoutPage() {
const { newCheckoutUi } = useFlags();
return newCheckoutUi
? <NewCheckout />
: <LegacyCheckout />;
}
function useFlag(flagName: string, defaultValue = false): boolean {
const [value, setValue] = useState(defaultValue);
useEffect(() => {
fetch(`/api/flags/${flagName}?user_id=${userId}`)
.then(r => r.json())
.then(d => setValue(d.enabled))
.catch(() => setValue(defaultValue));
}, [flagName]);
return value;
}
Flag Lifecycle Management
flags:
checkout_new_payment_ui_v2:
type: release
owner: checkout-team
status: rollout
percentage: 25
targeting:
- rule: plan == "pro"
serve: true
- rule: internal_user == true
serve: true
default: false
created: 2025-01-15
expires: 2025-04-01
cleanup_ticket: ENG-4521
metrics:
primary: checkout_completion_rate
guardrails: [error_rate, p95_latency]
Flag Evaluation Middleware (Express)
app.use(async (req, res, next) => {
req.flags = {
async get(flagName) {
return flags.isEnabled(flagName, {
userId: req.user?.id,
plan: req.user?.plan,
region: req.headers['cf-ipcountry'],
});
}
};
next();
});
app.get('/checkout', async (req, res) => {
const useNewUI = await req.flags.get('checkout_new_payment_ui_v2');
res.render(useNewUI ? 'checkout-v2' : 'checkout-v1');
});
Targeting Rules Examples
def evaluate_rules(flag: dict, context: dict) -> bool | None:
for rule in flag.get('rules', []):
if matches(rule['condition'], context):
return rule['serve']
return None
def matches(condition: str, context: dict) -> bool:
field, op, value = condition.split(None, 2)
ctx_val = context.get(field)
if op == '==': return str(ctx_val) == value
if op == '!=': return str(ctx_val) != value
if op == 'in': return str(ctx_val) in value.strip('[]').split(',')
if op == 'ends_with': return str(ctx_val ).endswith(value)
Cleanup Process
SELECT flag_name, reached_100_at, owner
FROM feature_flags
WHERE percentage = 100
AND reached_100_at < NOW() - INTERVAL '14 days'
AND status = 'on'
ORDER BY reached_100_at;
Monitoring and Alerts
from prometheus_client import Counter
flag_evaluations = Counter(
'feature_flag_evaluations_total',
'Feature flag evaluations',
['flag_name', 'result', 'targeting_rule']
)
Stale Flag Detection
def find_stale_flags():
used = set()
for file in glob('**/*.py'):
with open(file) as f:
matches = re.findall(r"is_enabled\(['\"]([^'\"]+)", f.read())
used.update(matches)
db_flags = {f.name for f in FeatureFlag.all()}
return db_flags - used
Targeting Strategies
Percentage Rollout
def is_enabled_percentage(flag: str, user_id: str, pct: int) -> bool:
import hashlib
hash_val = int(hashlib.md5(f"{flag}:{user_id}".encode()).hexdigest(), 16)
return (hash_val % 100) < pct
User Whitelist
beta_feature:
whitelist: [user_123, user_456]
Attribute-Based
premium_feature:
rules:
- if: user.tier == 'premium'
then: enabled
- if: user.country in ['US','CA']
then: enabled
Kill Switches
@app.route('/admin/flags/<flag>/disable', methods=['POST'])
@require_admin
def kill_switch(flag):
flag_obj.enabled = False
flag_obj.rollout_percentage = 0
cache.delete(f"flag:{flag}")
audit_log.record('emergency_disable', flag)
Auto-Disable on High Error Rate
if error_rate > 0.1:
flags.emergency_disable('problematic_feature')
alert('Feature auto-disabled: 10% error rate')
Anti-Patterns to Avoid
| Anti-pattern | Problem | Fix |
|---|
| Flag explosion | 200+ flags; nobody knows what each does | Expiry dates; cleanup tickets; registry with owners |
| No default value | Flag service outage causes unhandled path | Always define a safe default |
| Business logic inside flag | Flag check coupled to logic; hard to remove | Wrap the feature boundary, not the logic |
| Never cleaning up | Technical debt; confusing code | Cleanup ticket created when flag is created |
| Testing only the on path | Off path broken silently | CI tests must cover both flag states |
| Flags without metrics | No way to know if rollout is safe | Define primary metric before launch |
Rules
- Name flags descriptively —
new_checkout_ui_v2 not flag_123; include the team, feature, and version.
- Always define a default — the default must be the safe, current behavior; never an exception state.
- Test both flag states in CI — flag=off must be as tested as flag=on.
- Use consistent hashing for percentage rollouts — the same user must always get the same experience.
- Create the cleanup ticket when you create the flag — not after the rollout; flags must have expiry accountability.
- Monitor metrics at each rollout stage — do not advance to 100% without reviewing the primary metric.
- Kill switches default to off — ops toggles that disable a feature must default to feature-enabled (flag=off means feature=on).
- Never nest flags —
if flagA and flagB creates 4 code paths; design features to need one flag each.
- Flags are temporary — if a flag is permanent, it is a configuration option, not a feature flag.
- Flag service failure must be safe — your application must work if LaunchDarkly or your Redis is down.