AW Dev Rethought

🕵️ Debugging is like being the detective in a crime movie where you are also the murderer - Filipe Fortes

Product Engineering: When Feature Flags Become Technical Debt


Introduction:

Feature flags are one of the most genuinely useful tools in modern software delivery. They decouple deployment from release, allow gradual rollouts, enable A/B testing, and provide an emergency kill switch for features that behave unexpectedly in production. Teams that use them well ship more confidently and recover from problems faster than teams that do not.

But feature flags accumulate. A flag that was added for a controlled rollout three months ago is still in the codebase because nobody got around to removing it after the rollout completed. A flag that was added for an experiment that concluded is still there because the experiment results were inconclusive and the decision about what to do with the feature was deferred. A flag that was added for a customer-specific configuration has become load-bearing infrastructure that nobody fully understands anymore.

The codebase that was supposed to be made safer by feature flags has become harder to reason about, harder to test, and harder to modify — not because flags are bad but because flags without lifecycle management become technical debt that compounds quietly until it causes real problems.


Flags Without Expiry Dates Become Permanent:

The most common failure mode of feature flag management is the absence of any management at all. Flags are added when they are needed and removed when someone remembers to remove them — which in practice means they are rarely removed at all. The perceived risk of removing a flag is always higher than the perceived benefit, and the work of removal is always less urgent than the next feature being built.

A flag that has been in the codebase for six months is harder to remove than one that has been there for six days. The engineer who added it may have left the team. The context about why it exists may be undocumented. The branches of code it controls may have diverged enough from the main path that removing it requires careful analysis rather than a simple deletion.

Flags that are added without explicit expiry dates or removal criteria accumulate indefinitely. The only way to prevent this accumulation is to make expiry a requirement at flag creation time — every flag should have a documented reason for existing, a condition under which it will be removed, and an owner responsible for that removal.


Code Complexity Grows Exponentially With Flag Count:

A single feature flag adds two code paths — the flag is on, or the flag is off. Two feature flags add four possible combinations. Ten feature flags add over a thousand possible combinations. Most of these combinations will never be tested because the testing surface grows faster than any team can keep up with.

This combinatorial explosion means that as flag count grows, the confidence engineering teams have in their test coverage decreases. A test suite that covers the main path through the application may not cover the path that exists when flags A, C, and F are enabled while B, D, and E are disabled. Bugs that only manifest in specific flag combinations reach production because the combination was never tested.

The operational implication is equally significant. When a production incident occurs, diagnosing it requires understanding which flags are enabled for which users — because the bug may only exist in specific flag states. Debugging becomes an exercise in reconstructing the exact flag configuration that produced the failure, which is difficult without tooling specifically designed to capture it.


Long-Lived Flags Encode Forgotten Decisions:

Feature flags that persist long enough become documentation of decisions that were never made. A flag that controls an alternative implementation of a core feature represents a moment when the team could not decide between two approaches and deferred the decision by keeping both. A flag that enables a new user experience for a subset of users represents a rollout that was started but never completed.

These deferred decisions accumulate in the codebase as flags that nobody wants to touch because touching them requires making the decision that was deferred when the flag was created. The flag is not just technical debt — it is decision debt that has been encoded in the codebase and left for a future engineer to resolve without the context that existed when the flag was added.

Resolving long-lived flags requires reconstructing that context — understanding what the flag was for, what the current state of the decision is, and what the consequences of removing the flag in either direction would be. This investigation takes time that would not have been necessary if the decision had been made when the flag was created.


Flag Sprawl Creates Operational Risk:

In systems where feature flags control significant behaviour — rollout percentages, algorithm selection, infrastructure routing — the flag configuration becomes operational infrastructure that must be managed with the same care as any other production configuration. Flag sprawl makes this management significantly harder.

A system with hundreds of active flags is a system with hundreds of dimensions along which its behaviour can vary. Operators who need to understand current system behaviour must understand not just the code but the flag configuration that the code is running under. A flag that was set to enable a temporary workaround during an incident and never reset affects system behaviour indefinitely after the incident is resolved.

Flag configuration changes that are not tracked in version control, not reviewed before deployment, and not monitored for unintended consequences introduce the same risks as unreviewed code changes — with the additional complication that flag changes take effect immediately without a deployment and are harder to roll back than code changes.


Cleaning Up Flags Is Engineering Work That Needs to Be Prioritised:

Flag cleanup is consistently deprioritised because it produces no user-visible change and no new capability. The codebase becomes simpler and more testable, but these benefits are invisible to stakeholders who evaluate engineering output by features delivered. The result is that flag cleanup happens reactively — when flag sprawl has become painful enough to justify the investment — rather than proactively as ongoing maintenance.

Teams that treat flag cleanup as first-class engineering work — allocating explicit capacity for it, tracking flag age and usage, and removing flags as a normal part of completing the work they were created for — maintain codebases that remain understandable as they scale. Teams that treat cleanup as something to do when there is time accumulate flag debt that eventually requires a dedicated cleanup project that itself becomes a significant engineering investment.


Conclusion:

Feature flags are not technical debt by nature. They become technical debt through poor lifecycle management — flags added without expiry criteria, flags that outlive the decisions they were meant to support, and flag sprawl that grows faster than cleanup keeps pace with.

The teams that use feature flags most effectively treat flag removal as part of the same workflow as flag creation. Every flag has an owner, an expiry condition, and a removal plan from the moment it is added. The flags that exist in the codebase at any given time are flags that are actively serving a purpose — not flags that served a purpose three months ago and were never cleaned up. That discipline is what separates feature flags as a delivery tool from feature flags as a source of complexity that compounds until it causes real harm.


If this article helped you, you can support my work on AW Dev Rethought.


Rethought Relay:
Link copied!

Enjoyed this post?

Stay in the loop

New posts + weekly digest, straight to your inbox.

or

Create a free account

  • Save posts to your vault
  • Like posts & build history
  • New-post alerts

Comments

Add Your Comment

Comment Added!