The Flag That Outlived Its Change
Progressive rollout needs a dial, and expand-contract needs a way to switch readers from the old path to the new one. Both are the same mechanism underneath: a flag, checked at runtime, that decides which behavior you get. This is genuinely the thing that makes careful shipping possible — without it, every change is all-or-nothing and every rollback is a deploy. The flag is what turns a risky one-way step into a sequence you can walk back. That’s not a small thing, and the answer is never to use fewer of them.
The problem is what happens after. A flag is introduced for a specific window: roll from one percent to a hundred, watch the metrics, be able to bail. That window closes in a week. The flag stays for two years. Nobody removes it because removing it is work with no visible payoff, and because there’s a vague sense that keeping it around is the safe choice — what if we need to turn it off again? So it accretes, along with the fifty others like it, and now the codebase has a combinatorial space of behaviors that no one has ever tested and no one can enumerate.
What makes this worse than ordinary dead code is that a stale flag isn’t dead. It’s live configuration, readable and writable, and the branch behind it still compiles. Someone eventually flips it — during an incident, at 3 AM, because a runbook from eighteen months ago says to — and executes a code path that hasn’t run in production since the rollout finished. That path was correct once, against a schema and a set of assumptions that have both moved on. The flag didn’t preserve an escape hatch; it preserved a trap, and it looked exactly like an escape hatch right up until it was pulled.
The discipline that fixes this is the same one that makes deprecations finish: the flag needs an expiry from the moment it’s created. Not a hope, a date — and ideally something mechanical, a test that starts failing or a build warning that fires when a flag outlives its window. The cleanup should be part of the change, not a follow-up ticket, because a follow-up ticket is where cleanup goes to be silently deprioritized forever. If the flag is worth adding, the removal is part of what you’re committing to when you add it.
There’s a real distinction worth holding onto here, which is that some flags are supposed to be permanent — a kill switch for a dependency, a per-customer capability, an operational lever you actually intend to pull. Those are configuration, and they deserve documentation, testing, and a plan. The ones that hurt are the temporary flags that were never explicitly retired and drifted into being permanent by default. The failure isn’t having flags; it’s losing track of which kind you have. A flag with an owner and an end date is a tool. The same flag two years later, with neither, is just an untested branch waiting for someone to find it.