Code that never runs, and code that runs once a year
A Socratic walk-through of code that never runs, and code that runs once a year — reasoned out one step at a time, not lectured.
The question we started with
THE QUESTION #How do you tell the difference between a program that is dead and one that is merely rare?
You have instrumented a system for ninety days. One module never executed once. Delete it?
The instinct is yes, and the instinct is how organisations lose their year-end close. Ninety days of silence is compatible with two very different worlds: a module nothing can reach, and a module reached only on the last working day of December. Nothing in the observation itself separates them. So what evidence actually would?
Reasoning it through
REASONING #Notice first that we have two independent kinds of evidence available, and they fail in opposite directions.
The first is static: read the code, build a call graph from the declared entry points, and see what is unreachable. This is proof-shaped — if genuinely nothing calls a routine, no amount of waiting will change that. But its soundness rests on knowing every entry point and every call edge, and that is exactly what legacy systems break. A COBOL CALL whose target is a variable holding a program name resolved at run time is invisible to the graph. So is a CICS LINK to a computed transaction, a reflective dispatch in Java, a stored procedure invoked only from a JCL step nobody catalogued, a table of handler names read from a database. Every one of these produces a false "unreachable". Static analysis, in other words, over-reports death.
The second is dynamic: watch what actually runs. Coverage instrumentation, load-module usage monitors, scheduler history, transaction counters. This one cannot lie about what it saw — an execution observed is an execution. But it says nothing whatever about what it did not see. It under-reports life, and the size of that error is set entirely by how long you watched relative to how long the thing sleeps.
So ask the question that reframes the whole problem: what is the longest period a legitimate piece of this system can sleep? Not ninety days. Business systems carry cycles at every scale — monthly, quarterly, statutory annual returns, a leap-day branch that fires every fourth February, a rate-change path that waits for a regulator, a disaster-recovery routine that hopes never to run at all. The observation window is only meaningful when compared against that calendar, and for some code no window is long enough, because the triggering condition is not periodic at all.
Which suggests the two signals should be crossed rather than chosen between. Observed-and-reachable is simply live. Unobserved-and-unreachable is the honest delete candidate, because two differently-fallible methods agree. Reachable-but-unobserved is the dormant bucket — and the point is that this is not a verdict, it is a queue for further work. And the fourth cell? Observed but statically unreachable is not a paradox; it is a bug report about your analysis. Something called it, so an edge exists that your call graph missed, and you have just learned the graph is unsound in a way that discredits its other findings too.
Is there a way to convert dormancy into evidence rather than waiting? There is, and it is the practice that actually resolves these cases: instrument before removing. Leave the code in place, add a record at its entry point — a log line, a counter, an alert — and let it sit across a full business cycle plus margin. Now silence is a measurement rather than an absence. Some teams go further and make the removal reversible in stages: stop maintaining, then stop compiling into the build, then delete, with a rollback path at each step. The cost is patience; the cost of the alternative is discovering the gap at the worst possible moment, since rare code is disproportionately code that runs when something important is happening.
The analogy
THE ANALOGY #A house has a boiler, a smoke alarm, and a chimney. Over three months the boiler runs constantly, the smoke alarm never sounds, and the chimney carries nothing. Two of those three have been silent, but only one is unused: the alarm is silent because things are going well, and its silence is the whole point of owning it. Bricking up the chimney and unhooking the alarm are not the same decision, even though the observations are identical.
a house has few enough fittings that you can inspect each one and reason about its purpose, whereas a legacy estate has tens of thousands of modules and no one alive who knows what several thousand of them were for — so the real work is not judging one case well but sorting the population cheaply enough to afford judging the residue.
Clarifying the model
THE MODEL #Three refinements.
First, "dead code" quietly conflates two things. Unreachable code cannot execute at all. Reachable-but-never-taken code can execute but the condition has not held — an IF branch for a product line that was withdrawn but whose flag still exists in the data. The first is safe to remove once reachability is trustworthy. The second depends on a claim about data that may never have been checked, and data outlives product decisions routinely.
Second, absence of evidence is weak evidence of absence, but it is not zero. The strength scales with the window relative to the cycle, and it can be strengthened cheaply by widening the net rather than waiting longer — scheduler archives, old operator logs, backup catalogues, source-control history showing the last time anyone touched it. Those are proxies, not proof, and it is worth saying which ones you actually had.
Third, the residue is the deliverable. A pass like this rarely ends with a clean two-way split; it ends with a live set, a defensible delete set, and a dormant set with a documented reason for each item. Reporting the third as if it were part of the second is where the harm comes from.
A picture of it
THE PICTURE #How to readthe horizontal axis is what reading the code says, the vertical axis is what watching it says. Agreement sits on the diagonal — top right is safely live, bottom right is the defensible delete set. The bottom-right cell is the dormant queue, where the year-end job lives and where waiting or instrumenting is the only honest next move. The top-left cell should be empty: a module seen running that the call graph says is unreachable is evidence your call graph is missing edges, not evidence about that module.
What became clearer
WHAT CLEARED #Deadness is not a property you can observe; it is a conclusion you assemble from two signals that fail in opposite directions. Static reachability over-reports death because dynamic dispatch hides edges from it, and observation over-reports death because the window is always shorter than some legitimate cycle. Crossing them turns an unanswerable question into a sorted population — and the honest output includes a third pile, labelled dormant, that has not been decided yet.
Where to go next
ONWARD #- How to choose an observation window when the triggering condition is an event rather than a date.
- What it takes to make a call graph sound in the presence of dynamically resolved program names.
- Whether staged removal — unmaintain, unbuild, delete — is worth its bookkeeping on estates of this size.
Key terms
TERMS #| Term | What it means |
|---|---|
| Call graph | a model of which routines can invoke which others, built by reading source rather than running it. |
| Reachability analysis | deciding, from the call graph and a set of entry points, which code could possibly execute. |
| Dynamic dispatch | resolving the target of a call at run time, which hides the edge from static analysis. |
| Coverage instrumentation | recording which code paths actually executed during a period of observation. |
| Dormant code | code that is reachable and correct but whose triggering condition occurs rarely, such as annually. |
Every term the collection defines is gathered in the glossary.