Skip to main content
Removing 22 Percent of a Codebase in Eight Weeks
Back to blog

Removing 22 Percent of a Codebase in Eight Weeks

With roughly 185,000 lines of Python, a team maintained a backend service in production for about four years, so we used an incremental approach. The numbers from the early-access pilot are real. The company name is omitted because we did not ask permission to use it publicly. We report what we observed and learned.

The codebase contained roughly 185,000 lines of Python, maintained by five backend engineers. Over four years of shipping under pressure, the service had experienced two major product pivots, three rounds of significant feature work, and the ordinary technical debt that builds up on a small team.

The Initial Analysis

YAGNI's initial repository pass took about 22 minutes to index the code and construct its call graph. That was slightly longer than typical for a codebase of this size: the service relied heavily on a plugin architecture, so the analyzer had to follow registry patterns through several layers of indirection.

The first analysis flagged 847 functions and 23 modules as unreachable from any production entry point, with high confidence. Another 314 functions were flagged at medium confidence. Static analysis marked them unreachable, but adjacent code contained some unresolvable dynamic dispatch. We did not propose automated removal for the medium-confidence set.

Seeing 847 high-confidence dead functions left the team both surprised and skeptical. The volume exceeded their expectations, but they were certain some of the functions still had a use.

Validation Pass

The team spent about a week reviewing the high-confidence candidates before opening any pull requests. They split the list by module ownership, with each engineer examining candidates in the modules they knew best. The time was well spent.

The review found about 60 functions that were correctly marked unreachable from production but that the team wanted to retain. The reasons differed. Some were debug utilities engineers ran locally. A handful belonged to a module about to be extended, where the dead functions served as scaffolding for upcoming work. A few were in a test helper module used for manual testing outside the automated suite.

Those 60 went into .yagniignore. Human review cleared the remaining 787 functions for automated removal proposals.

Week 1 Through 4: The Easy Removals

The first four weeks produced the cleanest pull requests. YAGNI created 15 to 25 pull requests per week, batching removals in isolated modules without coupling complications. A typical pull request removed 3 to 8 functions from one module, passed the test run in full, and was reviewed and merged in under a day.

The total climbed quickly. By the end of week 4, 38,000 lines were gone. There were no production incidents and zero test failures in the CI pipeline after merges. Confidence in the process grew enough for review time to fall from half a day per PR to about 20 minutes.

One surprise was that about 30% of the removed lines consisted of comments and documentation for dead functions. Removing a function also removes its docstring and inline comments. Because the codebase had experienced significant engineer turnover, many of those comments reflected decisions from two or three years ago. Their removal reduced cognitive noise as well as runtime weight.

Weeks 5 Through 7: The Coupling Layer

The second phase proved harder. The remaining candidates lived in central utility modules and, as described earlier, brought afferent coupling complications. Several dead utility functions were imported by 10 to 20 different modules. Before deleting each one, the team had to clean up every import reference across those modules.

YAGNI automated the coordinated import cleanup. Pull requests in this phase took a different shape: one PR removed the dead function and its import references across all importing modules at the same time. Reviews were more careful because more files changed, but each PR still deleted code without changing behavior.

The team also found one actual mistake in this phase. YAGNI had flagged a function as dead with high confidence, but a third-party library called it indirectly while serializing a configuration object. The function name served as a callback key in the configuration. This was precisely the external interface dependency the validation pass should have caught. It did not, because the engineer responsible for that module did not know the library used function names as config keys.

Code review caught the mistake before production. The function went into .yagniignore, and the PR was closed. No incident. It reminded us that high confidence is not certainty and code review remains a meaningful gate.

Week 8: Final Count and Reflections

At the end of week 8, net removal reached 40,700 lines, about 22% of the starting line count. The 23 dead modules were removed entirely. Of the 787 cleared candidates, 71 appeared in later PRs after blockers were resolved or the team handled prerequisite coupling cleanup by hand in harder cases.

The test suite's build time fell by about 8%. Fewer files needed parsing, and several expensive test fixture setups were removed because they served only the deleted code paths.

Two engineers independently said that navigating the codebase felt different afterward. It was not dramatically faster, but it was less tiring. They had less context to hold while reading a module, and functions that existed for historical reasons were gone.

The pilot's main lesson is that mature codebases having large amounts of dead code is not an edge case. 22% is toward the high end of what we have seen across early-access repos, but not dramatically so. The issue is not whether the code exists. It is whether the removal process can handle it incrementally and carefully without breaking things.

Try YAGNI on your codebase. Free plan, no credit card required.

Get Early Access

More from the blog

View all articles