$ cat choices/feature-flags.md
the call
I decouple deploy from release with flags: ship code dark, turn it on by cohort, and kill a bad change with a toggle instead of a rollback. It's the runtime half of progressive delivery, and the discipline is deleting the flag before it becomes a permanent branch in your logic nobody dares touch.
Deploying and releasing are different events, and conflating them is most of why releases feel scary. Flags split them. The code ships continuously (small, integrated, low-risk) and the feature turns on when you decide, for 1%, for internal users, for one cohort at a time. A bad release stops being a 2am rollback and becomes a toggle. It’s the runtime complement to canary deploys: the dial that controls exposure and the kill switch that ends an incident in seconds instead of a redeploy.
Flags are debt with a half-life. A flag that should’ve been deleted three months ago is now a permanent fork every code path has to reason about, and a test matrix that doubles with each one. I use them for releases and experiments, then I remove them. That cleanup is the non-negotiable part, not an optional follow-up. I don’t flag everything, I don’t let “temporary” flags calcify into permanent config, and a flag system you don’t trust (stale targeting, mystery state) is its own outage vector.
The pattern: ship dark, then roll out 1% → cohort → 100%, gated by signal, killed on red. It’s the runtime half of the same progressive-delivery story that canary deploys handle at the infrastructure layer, and it leans on observability to decide whether to advance the dial or yank it. It’s also autonomy with guardrails in concrete form: a flag is what lets the team ship continuously (move fast) while keeping exposure controlled and instantly reversible (the rail that makes it safe).— see: choices / argo-cd · choices / observability
Separate the thing that feels irreversible (release) from the thing that’s routine (deploy), and put a dial and a switch on the part that matters. Control the blast radius at runtime, not just at deploy time, because the failures that hurt most are the ones you can’t turn off fast. The flag is cheap insurance; the bill comes due only if you forget to cancel the policy.
the gaps — what it costs even when it’s right
Flag debt is the big one. Every flag you don’t delete is a permanent branch in the logic and a doubling of the paths you have to reason about and test. The cleanup discipline is the practice. Skip it and you’ve traded deploy risk for a tangled runtime.
Combinatorial complexity. Two flags is four states; ten is a thousand. You can’t test the matrix exhaustively, so a rare combination becomes the bug nobody reproduced until a customer did.
The flag system is infrastructure now. Targeting, evaluation, and state are something you operate and have to trust. A mis-evaluated flag in a hot path is an outage, and “it was off, I swear” is a debugging session.