Skip to content

2 min read

The Cost of a Clever Abstraction

I once replaced four hundred lines of repetitive code with forty lines nobody could read. Here is the arithmetic I now do before doing that again.

There is a particular high that comes from collapsing repetition. You see the same shape five times, you feel the itch, and an hour later there is one generic function where five specific ones used to be. The diff is beautiful. It removes three hundred and sixty lines.

Six months later someone needs a sixth case that is almost the same, and you discover what you actually built.

What the repetition was doing

The five functions were not duplicated code. They were five independent decisions that happened, at that moment, to have the same shape. Duplication and coincidence look identical in a diff, and only one of them is safe to remove.

When I merged them, I asserted that the five would always change together. Nobody asked me whether that was true. The compiler certainly did not. And it was not true — three of the five had different owners, different release cadences, and eventually different requirements.

The arithmetic

Before collapsing repeated code I now ask three questions, in this order:

  1. If one of these changed, would the others have to? If the honest answer is "not necessarily", it is coincidence, not duplication.
  2. How many parameters does the shared version need? Every parameter is a place where the cases disagree. Past three, the abstraction is mostly a record of disagreement.
  3. Can I explain the shared thing in a sentence with no "or"? "Formats a currency amount" survives that test. "Formats a currency amount, or a date, or a percentage depending on the type flag" does not.

Three failures out of three is a decision. Two out of three is a conversation. Zero is a refactor worth doing.

Duplication is cheap to fix later and obvious when it hurts. The wrong abstraction is expensive to fix later and invisible until it does.

The version I would write now

Leave the five. Extract only the genuinely shared primitive — the currency formatter, the date parser, the retry loop — and let the five call it. You get the deduplication where the meaning is actually shared, and each caller keeps the right to diverge without asking permission.

This produces more lines. It is the correct number of lines.

The one exception

None of this applies inside a single module with a single owner, where the cost of being wrong is a ten-minute refactor by the person who wrote it. Collapse away. The rule earns its keep at boundaries — between teams, between services, between anything with its own deploy — where the wrong abstraction becomes a treaty rather than a function.