Feature Flags
decoupling "deployed" from "released"
Developer toolingdeveloper tooling / DevOps
The myth
"A feature flag is just an if-statement wrapped around new code. if (ENABLE_X) { newThing() } — done."
Actually
That's the mechanism, not the point. The value of a flag is that it moves the release decision OUT of your deploy pipeline and into a runtime switch. Deploying code and turning a feature on become two independent events — controlled by different people, at different times, without a rebuild.
Once you internalize that, four things stop being scary:
1. Ship dark, release later
You merge and deploy half-finished work behind an OFF flag. It's in prod, running through CI, but invisible. No long-lived branches rotting for 3 weeks, no monster merge conflicts. Trunk stays green.
2. Progressive rollout
Flip the flag for 1% of users, watch error rates and latency, then 5% → 25% → 100%. A bad release now hurts 1% for 5 minutes instead of everyone for an hour.
3. Kill switch
Feature melting prod at 2am? Flip it off. No hotfix, no redeploy, no waiting for a build. Recovery time drops from ~20 min to ~20 sec.
4. Targeting / experiments
Same plumbing lets you turn a feature on for internal staff, or a beta cohort, or run an A/B test — because evaluation happens per-request with context.
The shape that matters (not a bare boolean)
Real flags evaluate against the current user/request, not a global env var:
# Bad: decided once at boot, same for everyone
ENABLE_CHECKOUT_V2 = os.getenv("ENABLE_CHECKOUT_V2") == "true"
# Better: decided per request, with context
if flags.enabled("checkout_v2", user=req.user, default=False):
return checkout_v2(req)
return checkout_v1(req)
Notes that separate intermediate from beginner:
- Always pass a
default— if the flag service is down, you must fail to a known-safe value, not crash. - Evaluate ONCE per request and reuse the result. Re-checking mid-flow can flip behavior halfway through if a rollout changes.
The part nobody warns you about: flag debt
Flags are temporary by design, but they never get removed on their own. A codebase with 400 stale flags is a combinatorial nightmare — you can't reason about which of 2^400 states is actually live. Rules that keep you sane:
- Every release flag gets an owner and an expiry date at creation.
- When it hits 100% and sticks, delete the flag AND the dead branch. Closing the loop is the job, not an optional cleanup.
- Distinguish flag types: short-lived release flags (delete fast) vs long-lived ops flags / kill switches (keep, but document).
Common traps
- Flags controlling flags → untestable spaghetti. Keep them independent.
- Testing only the ON path. Your OFF path is what real users hit first — test both.
- Putting secrets or pricing logic client-side behind a flag. Client flags are visible to users; gate anything sensitive on the server.
Your exercise
Take one if (env.SOMETHING) toggle in your codebase. Ask: (1) Could I flip it without redeploying? (2) If the flag backend returned an error, what happens — safe default or crash? (3) Who owns it and when does it get deleted? If you can't answer #3, that's flag debt waiting to happen. Write the answers down.