When removing duplication hurts
A Socratic walk-through of when removing duplication hurts — reasoned out one step at a time, not lectured.
The question we started with
THE QUESTION #Why does merging two identical pieces of code sometimes make a system harder to change?
Almost the first rule anyone learns is: do not repeat yourself. Two identical blocks of code are two places to fix a bug, two places to forget. Extract the shared part, call it from both, and the system is smaller and safer. The reasoning is sound and the habit is usually right.
And yet experienced people will look at two identical functions and deliberately leave them alone. Some of them will tell you that duplication is cheaper than the wrong abstraction. That is a strange thing to say about a rule this well established — so what do they know that the rule does not say?
Reasoning it through
REASONING #Start with what the extraction actually does, mechanically. Before it, two call sites each own their behaviour outright. After it, both depend on one function, and that function must satisfy both. Nothing was deleted; a constraint was added — the two behaviours are now required to stay equal.
So the honest question is not "are these two blocks the same today?" but "will these two blocks always want to change together?" Those are different questions, and only the second justifies the constraint.
Consider what happens when the answer is no. A requirement arrives that changes one caller's behaviour and not the other's. The shared function cannot simply be edited, because that would change both. What does the next person do? Almost always the cheapest thing: add a parameter. A boolean, perhaps, and a branch inside. Now the function does two things depending on a flag, and its name — chosen when it did one thing — is a mild lie.
Then it happens again. A second flag, a second branch. The parameter list grows, the branches interact, and the function's body becomes a small machine for reconstituting the two distinct behaviours you began with, from the inside. Anyone who wants to change one caller must now read the whole thing and reason about the other caller too. That is strictly worse than the original duplication, because the original duplication at least let you read one case in isolation.
Notice why the trap is hard to spot at the moment of extraction. The two blocks looked identical, and identical text is very persuasive. But sameness of text is not sameness of reason. Two functions can compute the same thing because they encode the same rule — or because two unrelated rules currently happen to produce the same answer. Call the second kind coincidental duplication. It is not a symptom of anything; it is a coincidence, and coincidences end.
This also explains why the original formulation of the rule is narrower than the slogan. Hunt and Thomas wrote that every piece of knowledge must have a single, unambiguous, authoritative representation. Knowledge, not characters. A tax rate is knowledge; if it appears twice, that really is a defect, because the two copies are the same fact and will always change together. Two validation routines that both happen to check a string is non-empty are not one fact repeated — they are two facts that agree at the moment.
So there is a test, and it is about the future rather than the present: if this rule changed, would every copy have to change? If yes, merge. If you cannot answer, you do not yet have the evidence — which is why the rule of three exists. Wait for a third occurrence, because two points fit any line and three start to reveal the actual shape of the commonality.
One more asymmetry decides the whole matter. Duplicated code that turns out to be genuinely shared is easy to fix later: you have all the cases in front of you, and merging them is a mechanical edit. A wrong abstraction that has acquired ten call sites is not easy to fix, because unwinding it means understanding what each caller actually needed, which the abstraction has spent its life obscuring. The two mistakes cost very different amounts, and that asymmetry — not a dislike of tidiness — is what makes waiting the better bet.
The analogy
THE ANALOGY #Think of it like two neighbours who both walk to the same station each morning. Noticing the overlap, they agree to share a car. While the routine holds, it saves fuel. But now neither can leave early, work late, or stop off on the way without negotiating — their schedules are coupled, and the coupling was invisible on the day they signed up, because on that day their schedules already matched.
two neighbours can dissolve a car share in one conversation, whereas a shared abstraction with many callers cannot be dissolved by anyone in particular — there is no longer a single person who knows what all the callers needed, which is exactly why the wrong abstraction outlives the duplication it replaced.
Clarifying the model
THE MODEL #Three clarifications, because this argument is easy to over-read.
First, this is not a licence for copy-paste. Genuine repeated knowledge — a business rule, a constant, a protocol format, a schema — should exist once, and the cost of not merging it is real bugs, not merely untidiness. The claim is narrower: identical text is weak evidence of shared knowledge.
Second, the failure is not "abstraction" but abstraction chosen too early, on too few examples. The same extraction made after the third or fourth call site, when you can see which parts genuinely vary, often produces a clean interface — because now the parameters correspond to real dimensions of variation rather than to guesses.
Third, there is a reliable symptom to watch for once you have merged. If callers start passing flags that switch behaviour, or if the shared function's name no longer describes what it does for every caller, the abstraction is being asked to serve two masters. That is the moment to inline it and re-split, while the number of callers is still small — an operation that gets monotonically more expensive from then on.
A picture of it
THE PICTURE #How to readEach box is a condition the code is in, and each arrow is an event that moves it. The self-loop on the top state is the benign case people fear: the copies drift apart, cheaply. The path down the middle is the expensive one — extraction, then a flag, then more flags — and note that the tangled state has only a self-loop leaving it, which is the whole point: it is easy to enter and has no cheap exit. The one escape runs through re-splitting while the caller count is still small.
What became clearer
WHAT CLEARED #Removing duplication does not delete code; it asserts that two things must always be equal. That assertion is valuable when the two copies encode one piece of knowledge, and corrosive when they merely agree by coincidence, because the system then spends years reconstructing the difference through flags. Identical text is the weakest possible evidence for that assertion — the real evidence is whether the two would have to change together — and since a premature merge is far harder to undo than a delayed one, waiting for a third example is not laziness but a rational bet on which mistake is cheaper.
Where to go next
ONWARD #- How connascence gives a finer vocabulary than "coupled or not" for what two call sites actually share.
- Whether the same asymmetry argues for waiting before extracting a shared service across team boundaries.
Key terms
TERMS #| Term | What it means |
|---|---|
| DRY | the principle that every piece of knowledge should have one authoritative representation in a system; often misquoted as a ban on repeated text. |
| Coincidental duplication | code that is identical because two unrelated rules currently produce the same result, rather than because they express one rule. |
| Rule of three | the heuristic of waiting for a third occurrence before extracting a shared abstraction, so the axis of variation is visible. |
| Inlining | folding a shared function back into its callers, the usual first step in undoing a premature abstraction. |
Every term the collection defines is gathered in the glossary.