← The ADLC library
Live debates · 63

The dead abstraction is now a worked example

Unused code used to be inert. It sat there costing nothing until somebody read it. In a repository that agents read on every ticket, a dead abstraction is a teaching aid, and you are paying for the lesson every time.

Every codebase over three years old has a cupboard. A caching layer somebody built for a launch that never happened. Two competing date helpers. A base class with one remaining subclass and a comment saying “TODO: collapse this”. A feature flag framework that predates the one you use now.

The traditional view is that this is untidy but cheap. It compiles, it is not on any hot path, nobody has to read it, and there is always something more valuable to do than delete it. That view was correct for twenty years.

It stopped being correct, and the reason is not disk space or build time. My position is that dead code has changed category: it is no longer inert storage, it is instruction. Anything sitting in your repository is a worked example of how this team writes software, and you are now paying to have that example read, weighted and imitated on every single ticket.

Prevalence is the only tiebreaker available

An agent working in your codebase infers your conventions from your codebase. That is the whole mechanism, and it is why the results are so good when the codebase is coherent.

It also means the inference is statistical. Given two ways of doing something, both present, the one with more instances wins. There is no field in the source tree marked “this one is the mistake”. A deprecated helper with forty call sites reads as the house style. The three call sites of the pattern you agreed on in April read as the exception.

So the cupboard is not neutral storage. It is a vote, and it is a vote against your current decisions, cast by code that nobody has run in two years.

Dead code, as it used to be priced

  • Cost paid once, by a human who happened to open it
  • That human could tell it was dead in seconds
  • They routed around it and forgot
  • Risk: mild confusion during onboarding

Dead code, as it is priced now

  • Cost paid on every ticket that touches the area
  • The reader cannot tell live from dead
  • It is copied forward as the local convention
  • Risk: your abandoned pattern spreads faster than your current one
The change is not in the code. It is in who reads it and how often. A one-time cost became a recurring one, and recurring costs compound.

The signal a human uses is not in the file

Ask an experienced engineer how they know a module is dead. They will say things that have nothing to do with its contents. Nobody mentions it in standup. The person who wrote it left. It was not in the last three incident threads. The directory feels old. The last commit message says “wip”.

None of that is in the file, and most of it is not anywhere a tool can reach. What the file itself shows is a competent implementation with reasonable names and passing tests, which is exactly what a live module looks like.

This is why “the agent should just tell” is not a fix. Deadness is a social fact about your organisation that happens to be expressed as an absence of activity. Absence is very hard to read and very easy to misread. A seasonal batch job that runs in January looks identical to something abandoned in 2023, right up until you delete it in June.

The three states, and the missing one

There are really three states a piece of code can be in, and most repositories only encode two of them.

LiveIn use, current, this is the pattern to follow. The repository can express this: it is just code.
GoneDeleted. Unambiguous, retrievable from history, costs nothing on any future ticket.
Present but not exemplaryStill called, still needed, and explicitly not the thing to copy. Almost no repository has a way to say this, so it defaults to reading as Live.
Most of the damage lives in the third row. It is the state that cannot be expressed by the presence or absence of a file, and the one teams have the least practice at declaring.

The first move is the obvious one and it is still the best one: if it is gone, make it gone. Deletion is the only intervention that reduces the recurring cost to zero, and version control means you have not lost anything. Teams hesitate because deletion feels irreversible and it is precisely the operation that is not.

The second move matters more than it used to. For the code that must stay but must not spread, say so, in the file, in a form that survives being read out of context. A one-line marker at the top of a module travels with the module into the window. A decision record in a wiki does not.

This is boring and it is not a product purchase. A deprecation banner as the first line of the file, naming the replacement, does most of the work. What it buys is that the strongest local signal about the module is no longer its forty call sites.

What this costs, honestly

The platform argument for cleanup has always been weak because the benefit is diffuse and the cost is a sprint nobody wants to spend. I am not going to pretend that changed.

What changed is where the benefit lands. Cleanup used to buy comprehension for future humans, which is real and impossible to defend in a planning meeting. Now it also buys a measurable reduction in a recurring input: less material to read on every task in that area, and fewer wrong-pattern changes arriving in review, which is the expensive end.

If you want the case in one sentence for someone holding a budget: the reason to delete the cupboard is not tidiness, it is that you are currently paying to have it read aloud to every agent, several times a day, as an example of good practice.

Where this breaks down

Deletion has a real failure mode and it is not hypothetical. Removing something that turns out to be load-bearing at quarter end is a genuine incident, and “version control means nothing is lost” is a glib answer when the loss is four hours of production downtime. The people who resist deletion have usually been through this. The discipline is not “delete more”, it is “establish deadness properly, then delete”, and the establishing is the expensive part that this argument tends to wave at.

Agents are getting better at ignoring irrelevant material. Recency weighting, call-graph awareness and better instruction-following all cut into this. The failure I am describing is real today and I would expect it to soften. If your evaluation is based on behaviour you saw a year ago, re-test it before spending a sprint.

A deprecation banner is a comment, and comments rot. The move I am recommending has the same weakness as every other in-file convention: someone marks a module deprecated, the replacement gets abandoned, and two years later you have a misleading banner on a module that is now the only live path. That is worse than no banner. If you cannot maintain the markers, deleting is the only version of this that stays true.

The prevalence argument cuts against new patterns generally. If the codebase votes by count, then your new standard is at a disadvantage even in a spotless repository with no dead code at all. Cleaning the cupboard does not fix that, and treating cleanup as the answer may mean skipping the harder work of making the current standard explicit somewhere other than in the count of its call sites.

Some teams genuinely have better things to do. If you are pre-fit, changing direction monthly, and your repository is eighteen months old, the cupboard is small and the compounding has not started. This argument gets stronger with codebase age and team size, and I would not spend a sprint on it in year one.

The takeaway

Dead code stopped being free the moment your repository became the primary source of instruction for how changes get made. It is not clutter, it is a worked example, and it is competing with your current standards on volume.

Delete what is genuinely gone, because that is the only move that reduces the cost to zero. For what has to stay and must not be copied, put the warning where it will be read: in the file, at the top, naming what to use instead.

If you take one thing into next week: open the last three agent changes you rejected for using the wrong pattern, and find where that pattern lives. You will usually find it is something you have been meaning to delete since before this project started.