When a feature flag is retired, we ask engineering teams how much dead code they expect. The answers usually are "some" and "probably not that much." Our first repository analysis nearly always finds more, sometimes much more.
This is not a knowledge failure. Engineers understand the particular things they know about, including the A/B test module they planned to remove and the unused webhook handler. What they misjudge is the pace. Dead code grows faster than expected because its causes are structural rather than accidental.
The Feature Flag Problem
Feature flags are the largest single source of dead code accumulation in the codebases we have analyzed. The sequence is familiar: a flag supports a rollout, the rollout reaches 100%, and nobody removes the flag. The "flag off" branch can no longer run because evaluation always returns true, but it remains because cleanup never made the roadmap.
Now extend that across a product's lifetime. A service running for three years and shipping a meaningful feature every few weeks may have dozens of completed rollouts behind it. If even half created flag cleanup debt, and most do, the total becomes significant.
The reverse happens often too: a flag is rolled out, then rolled back, and the feature is abandoned. Its "flag on" branch is dead. Removing it requires knowing what the feature was meant to do and verifying that no rollback is planned, so the code remains.
A/B Test Residue
A/B testing systems leave much the same residue. An experiment runs for four weeks, a winner is chosen, and the winning variant becomes standard behavior. The experiment infrastructure, including both variants and the splitting logic, often remains. Since the experiment has ended, nobody schedules cleanup before moving to the next thing.
In repositories where product experimentation stays active for years, A/B test residue can become substantial. We have seen codebases with 8 to 12% of total function count coming from experiments concluded more than a year ago. None caused harm, but all expanded maintenance surface, confused new engineers reading the code, and kept tests for behaviors that no longer existed.
Product Direction Changes
When a team pivots or deprioritizes a feature, its code remains in the repository. This category is less clear than feature flags or A/B tests because the code may not be dead. The feature could still be on the backlog for eventual revival. "We might need this later" keeps plenty of code alive beyond its useful life.
The practical test is straightforward: code that has sat in the repository for 18 months without production traffic or recent modifications, while its feature was explicitly deprioritized, has a probability approaching zero of being revived in its current form. Codebases change. When the feature returns, surrounding code will have changed enough that the old implementation would need rewriting anyway. Keeping it "just in case" exchanges ongoing maintenance cost for a benefit unlikely to appear in the expected form.
Dependency Version Transitions
Major dependency upgrades often leave dead code in the adapter layer. When a team moves from one library version to another and the interface changes, it usually adds a compatibility shim or adapter for old callsites. After every callsite uses the new interface directly, the shim is obsolete. It often remains because nobody schedules removal once the migration is complete.
Database migrations follow the same pattern. When a schema change replaces old columns or tables, code reading from or writing to those structures becomes dead. The migration runs, the data moves, and the old code remains.
The Copy-Forward Pattern
A less visible accumulation mechanism is the copy-forward pattern. An engineer needs a function like an existing one, with slight changes. Instead of extending the original, they create a new version. The old function has callers, so it is not deleted. Eventually those callers move to the new function, but the old one remains because nobody wants to audit every callsite to verify the migration.
The same pattern can encompass whole modules. A service is rewritten, the new module goes live, and the old one stays "in case something breaks." Six months later, nothing has broken. The rewrite is production code and the old module is dead, yet deleting it feels riskier because removal seems capable of undoing the migration.
The Accumulation Rate
These mechanisms are not occasional. They run continuously. Each sprint that ships a feature flag creates cleanup debt. Each completed experiment leaves residue. Each dependency upgrade produces adapters that outlast their purpose.
Across three years, an actively shipping team can easily reach 10 to 20% dead code by line count from these mechanisms alone, without doing anything wrong or being careless. This does not indicate a broken process. It is what happens when shipping outranks cleanup, which is most of the time, as it should be.
Cleanup loses because the next feature is always more urgent. Once someone chooses to handle it manually, the volume may make it a multi-week project with real risk of removing live code by mistake. Then the work remains undone.
This is the gap YAGNI was built to close. Dead code accumulation is structural and will continue, but removing it on an ongoing basis keeps it from becoming a multi-week cleanup project.
Try YAGNI on your codebase. Free plan, no credit card required.
Get Early Access