Feature flags look cheap: wrap an if block, deploy, and you can toggle a risky feature in production instantly. The catch is that no team ever prioritizes removing them — six months later your codebase has forty nested conditionals nobody remembers the purpose of. This piece argues the opposite of the usual pitch: flags aren't the problem, a flag with no removal date is.
Martin Fowler's original writing on the subject makes the same point — flags introduce a new abstraction and a real testing burden, and they multiply fast if nobody actively manages them. Here we cover flag types, which should be short-lived versus long-lived, naming and ownership discipline, a cleanup workflow, and practical ways to test both branches of a flag.
Flag types: they don't all belong in the same bucket
Not every flag has the same lifecycle. Release flags exist to deploy unfinished work to production while hiding it from users; once the feature ships, they should be removed — their lifespan is measured in days or weeks. Operational (kill switch) flags let you roll back behavior without a deploy when a dependency breaks or new code misbehaves; these can legitimately live as long as the risky integration does. Experiment flags live for the duration of an A/B test and get retired once a decision is made. Permission/entitlement flags control which user segment sees which feature and are usually a permanent part of the product.
Treating all four the same way, under one rule, is where teams go wrong. As Fowler recommends, release flags and permission/business flags deserve separate configuration files and separate lifecycle rules.
Why unmanaged flags become combinatorial test debt
Two independent flags mean four states to test; five flags push that to 32. In practice nobody tests all of them — the "usually on" state gets covered and the rest of the combinations get discovered in production. That's essentially what happened in Knight Capital's infamous 2012 incident: a stale flag re-activating unexpectedly triggered a $460 million loss in minutes. The lesson holds: every flag still alive is a permanent multiplier on your test matrix, whether or not you're actively testing every combination.
Naming, ownership, and an expiry date
The rule that actually holds up in practice: every flag gets an owner, a stated purpose, and an expiry date the moment it's created. For release flags, that expiry shouldn't stretch past a few weeks — Fowler's guidance points the same direction. On naming, swap vague names like enable-new-checkout for date- and purpose-bearing ones like checkout-v2-release-2026-07, which kills the "what was this flag even for?" question six months out.
Flag type | Typical lifespan | Ownership | Removal trigger |
|---|---|---|---|
Release | Days–weeks | Team shipping the feature | Feature stable at 100% rollout |
Kill switch (ops) | As long as the risky integration | Platform/SRE team | Dependency becomes trusted |
Experiment (A/B) | Length of the test window | Product/data team | Decision made |
Permission/entitlement | Permanent | Product team | Product decision changes |
A cleanup workflow that catches stale flags
The most practical habit: dropping a "remove this flag" task onto the backlog the moment a release flag is created — in the same PR, not a separate ticket. Some teams add an expiry field to their flag-creation process and run a simple CI check that warns once that date passes; if you keep flags in one source of truth like a flags.yaml, that's an afternoon of work. Static analysis that flags conditions which never evaluate to false (or always to true) is also a cheap way to catch dead flags nobody remembered to remove.
The hard part here isn't tooling, honestly — it's discipline. "We'll clean it up later" is the first casualty of sprint pressure. Attaching the removal task to the same PR that introduces the flag holds up far better than deferring cleanup to a separate tech-debt sprint that never gets prioritized.
Testing both branches
If behavior differs when a release flag is on versus off, both states need CI coverage — otherwise flipping the flag ships you straight into an untested code path. The practical approach is treating flag state as a test parameter and running the flag-critical tests in both states, but only those tests, not your entire suite — otherwise your CI time grows combinatorially. This is essentially a flag-specific application of the "no untested code path reaches production" principle we cover in our clean code principles checklist.
Flags can also be part of your zero-downtime deployment strategy: decoupling deploy from release is one of the cheapest ways to ship a risky change and ramp traffic gradually. But that power should come paired with trunk-based development discipline — flags don't replace long-lived branches, they make them unnecessary.
When you need to roll flags out alongside a schema change, pairing flag-based release with the expand-contract pattern from our zero-downtime database schema migrations guide keeps both the code and schema side safely reversible.
If you'd rather not tie your flags to a large SaaS vendor, one of the self-hosted tools we cover in our open-source SaaS alternatives guide (Unleash or Flagsmith, for instance) covers this need directly; paired with OpenFeature, switching providers later only touches your provider configuration, not your application code.
Pairing flags with observability
If flipping a flag on quietly raises your error rate or latency, the fastest way to notice is attaching flag state to your traces and metrics as an attribute. Tagging every span with something like feature.checkout_v2 = true/false lets you compare performance side by side, on versus off, in the dashboards we describe in our getting started with OpenTelemetry guide. It's a cheap, frequently skipped practice that heads off the "we flipped the flag, error rates crept up a few days later, and we couldn't tell why" scenario.
For more software engineering practices, browse our Software Engineering category.
Frequently Asked Questions
Are feature flags actually tech debt, or a good practice?
Both, depending on discipline. The flag itself is harmless; what's harmful is it staying in the codebase forever with no expiry date or owner. Flags managed with a disciplined lifecycle are one of the safest ways to decouple deploy from release.
How many flags counts as "too many"?
There's no hard number, but a practical signal is this: if a new engineer opens a file and struggles to follow the nested flag conditions, or tracing a bug report to a specific flag combination takes minutes instead of seconds, it's cleanup time.
Should kill-switch flags get an expiry date too?
Not a hard date, but a review trigger — the flag's necessity should be re-evaluated once the dependency it protects against is considered trustworthy again, or a replacement solution ships.
Should I use a standard like OpenFeature for flag management?
If you might switch flag providers or want to avoid vendor lock-in, yes — OpenFeature gives your application code a common evaluation API without tying it to one specific flag management vendor.



