Feature Flags
Flag Types
| Type | Purpose | Lifespan | Example |
|---|---|---|---|
| Release flag | Hide incomplete features | Short (days–weeks) | new_checkout_flow |
| Experiment flag | A/B test variants | Short (experiment duration) | pricing_page_variant_b |
| Ops flag | Runtime behavior control | Long (permanent) | enable_rate_limiting |
| Permission flag | Entitlement by user/plan | Long (business-driven) | feature_advanced_analytics |
Gradual Rollout Strategy
1% → Internal users / employees only
5% → Alpha users / trusted beta
25% → Broader beta cohort
50% → Half of traffic (monitor metrics)
100% → Full rollout → schedule flag cleanup
Rollout Criteria at Each Gate
- Error rate < baseline + 0.5%
- Latency p99 within 10% of control
- No critical bug reports
- Key business metrics not degrading
Flag Naming Conventions
Format: <scope>_<feature>_<variant?>
Good:
checkout_v2_enabled
pricing_annual_discount_variant_b
api_rate_limit_enabled
Bad:
flag1
newFeature
CHECKOUT_V2 (avoid ALL_CAPS)
Naming Rules
- Lowercase with underscores
- Start with scope/domain prefix
- Boolean flags: use
_enabledsuffix - Experiment flags: use
_variant_[a|b|c]suffix - Avoid negatives:
disable_Xcreates double-negative confusion
Kill Switch Design
A kill switch is an ops flag that defaults to ON and disables a feature when flipped OFF.
# Pattern: default ON, flip OFF to disable
if feature_flags.is_enabled("payment_processing_enabled", default=True):
process_payment(order)
else:
return ServiceUnavailableError("Payment processing temporarily unavailable")
Kill Switch Checklist
- Default value is
True(feature active) - Graceful degradation path defined
- User-facing error message ready
- Runbook documented: when to flip, who can flip, how to re-enable
- Alert wired to flag state change
Flag Lifecycle Management
CREATED → ACTIVE → RAMPING → FULLY_ROLLED_OUT → DEPRECATED → REMOVED
Lifecycle Gates
| Stage | Action | Owner |
|---|---|---|
| Created | Add to registry, set default, document intent | Engineer |
| Active | Gradual rollout begins, metrics monitored | PM + Engineer |
| Fully rolled out | 100% traffic, stable for ≥ 2 weeks | PM |
| Deprecated | Removal ticket created, countdown timer set | Engineer |
| Removed | Code deleted, flag removed from system | Engineer |
Stale Flag Detection
Flag is stale when:
- Age > 90 days with no rollout changes
- 100% rollout for > 30 days (release flags)
- Experiment concluded but flag not removed
Governance Checklist
Before creating a flag:
- Flag type selected (release/experiment/ops/permission)
- Owner assigned (PM + Engineer)
- Removal date or criteria defined
- Fallback behavior documented
- Metrics to monitor during rollout identified
Before full rollout:
- Rollout gates passed (error rate, latency, metrics)
- Removal ticket created in backlog
- Cleanup PR planned for next sprint
LaunchDarkly vs Unleash vs Homegrown
| LaunchDarkly | Unleash | Homegrown | |
|---|---|---|---|
| Setup time | Hours | Days | Weeks–months |
| Cost | $$$$ | Free (OSS) / $$ (cloud) | Engineering time |
| Targeting | Advanced (segments, rules) | Good | You build it |
| Analytics | Built-in | Limited | You build it |
| Audit log | Yes | Yes | You build it |
| Best for | Scale + compliance needs | Cost-conscious / OSS preference | Simple use cases only |
Decision Matrix
- < 5 flags, simple on/off → Homegrown (env vars)
- Growth stage, experiment-heavy → Unleash OSS
- Enterprise, compliance, advanced targeting → LaunchDarkly
Technical Debt from Flags
Flag Debt Accumulates When
- Experiments conclude but flags remain in code
- 100%-rolled-out flags are not cleaned up
- Flags reference deleted features
- Flag logic creates complex conditionals
Cleanup Process
- Audit flags monthly (automated report from registry)
- Assign cleanup tickets for all deprecated flags
- Remove flag evaluation + dead code branch in same PR
- Update tests — remove flag-conditional test variants
- Verify no references remain (
grep -r "flag_name" .)
Cost of Flag Debt
- Each live flag adds a conditional branch in production code
- 50+ stale flags = significant cognitive overhead
- Nested flag conditionals are extremely hard to reason about
- Rule: delete the losing variant's code; don't just remove the flag check