Skip to main content
Why Tight Coupling Hinders Dead Code Removal
Back to blog

Why Tight Coupling Hinders Dead Code Removal

Code review alone can miss what coupling analysis reveals: a dead function may be difficult to remove. When we scan a new repository with YAGNI, we inspect module dependencies before any individual function. They show removal difficulty, not what is dead.

Loose module coupling yields a focused set of removal candidates with a predictable blast radius. When everything imports everything else, the dead-function list may be identical, but every candidate sits among dependencies that need auditing before deletion is safe. Coupling says how much work acting on dead code requires, not what is dead.

The Neighbor Problem

Tight coupling creates what I call the neighbor problem: a dead function woven deeply into live code cannot leave without changes to that live code. It may do nothing in production, while nearby live code still has an import for it, a type reference, or a parameter list containing it as an optional argument. Deleting the dead function means changing the live code too.

That does not mean the dead code should stay forever. It means the work needs a different sequence. First resolve the coupling to live code, then remove the dead function. That first part is refactoring, not deletion.

YAGNI calls out these coupling dependencies directly. If a proposed removal also touches adjacent live code, it presents that work separately, making clear to the reviewer which part deletes code and which part prepares for deletion.

Afferent Coupling: The Real Removal Barrier

Software design metrics separate afferent coupling, the number of things depending on a module, from efferent coupling, the number of things it depends on. Afferent coupling is the relevant metric for dead code removal.

A dead module with high afferent coupling, meaning many modules import from it, may be completely unreachable at runtime and still be impossible to remove. Those live-module imports create compilation or resolution dependencies. Deleting the module breaks the build even when no code path calls it.

This often happens with utility modules in mature codebases. A utilities.py or helpers.ts file may hold 40 functions, with 35 never called. Yet 60 modules import the file. Some imports use the five live functions; others import the module and use dead functions made unused by newer utilities. The file must remain, but 35 of its 40 functions can go after the imports referencing them are cleaned up.

Unused-import cleanup must come before function removal; it is not a side effect. For a high-afferent-coupling file, the sequence is: audit every importer, remove references to dead functions, then delete those functions.

Circular Dependencies and the Removal Wall

Circular dependencies form a coupling pattern that puts a hard wall around removal work. If module A imports module B and module B imports module A, removing anything from either requires examining both at once. Static analysis tools must track the graph's cycle explicitly to prevent infinite traversal.

For removal, the key issue is that a dead function inside such a cluster cannot be removed by changing only its own module. Because the cycle acts as a unit, removal must be coordinated across it. Parts of something that depends on itself cannot be removed independently.

We have encountered circular dependency cycles involving four, five, and occasionally more modules in production codebases. Each is a zone where automated removal needs extra care. YAGNI proposes removals there but marks them for coordinated review instead of independent pull requests.

The Practical Shape of Coupling in Old Codebases

After code has accumulated for five or more years, its coupling topology often takes a familiar shape. A central cluster of highly-connected utility modules usually sits in the dependency graph's middle, imported everywhere. Feature-module groups surround it, importing the utilities but not one another. Entry points, including handlers, workers, and scheduled jobs, sit at the edges.

Edge removals are usually easiest. Feature modules created for a cancelled product direction and no longer referenced by an entry point are structurally isolated. Removing one does not require changing the central utilities. Its dependency edges point inward, so deleting the whole module only requires cleaning imports in the center that referenced it.

Central utility clusters contain the hardest removals. Usage counts may show a function is dead, while import statements across dozens of modules protect it. Removing it means auditing all 60 or 80 importers to see which actually reference it.

Using Coupling Data to Sequence Work

For each repository removal plan, we rank proposals with a "removal readiness" score. It combines confidence that a function is dead with the scope of coordinated changes needed for safe removal.

High readiness means dead with high confidence, no live afferent dependencies, and no circular dependency cluster. These pull requests open cleanly, pass tests, and change only dead code.

Medium readiness means dead with high confidence, with afferent dependencies that are also dead or have import references that are easy to prune. A small amount of coordinated cleanup, usually deleting unused import lines, comes before the deletion.

Low readiness means dead with high confidence but deeply embedded in a circular dependency cluster, or dependent on live code that must be understood before pruning. We surface these for human-led refactoring instead of automated removal.

Teams removing dead code without this sequencing often meet the same wall: early removals go smoothly, then one dead function needs three other things cleaned up, each of which needs two more, and the "quick cleanup" becomes a two-week project. Coupling data exposes that structure before work begins.

Where Automated Removal Has Limits

Tight coupling is not a reason to stop removing dead code. It is a reason to distinguish precisely between work we automate and work requiring engineering judgment.

Automated removal handles edge cases well: isolated feature modules, clearly unused utilities with no remaining callers, and dead code in modules that are themselves unreachable. The central, heavily-coupled utility layer needs another path: expose the dead functions, let engineers choose the refactoring sequence, then automate removal after the prerequisites clear.

The aim is not to automate everything. We automate obvious cases reliably and make non-obvious cases visible enough for the right engineering work to happen in the right order.

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

Get Early Access

More from the blog

View all articles