Every developer learns DRY early and takes it to heart: don’t repeat yourself, factor out the duplication, name the shared thing. It’s good advice, mostly. But it curdles into a reflex — see two bits of code that look alike, and abstract them on the spot — and that reflex quietly does more damage than the duplication it was meant to cure.
Two lines that look the same aren’t necessarily the same
Here’s the trap. Two pieces of code look identical today, so you pull them into a shared function. For a while it’s tidy. Then one caller needs the behaviour to differ slightly, so you add a parameter. Then another caller needs a different tweak, so you add a flag. Six months later the “shared” function is a knot of booleans and special cases, and every change to it means checking four callers that were never really doing the same thing — they just briefly looked alike.
The mistake was reading incidental similarity as essential similarity. Two calculations that both happen to multiply by 1.2 today aren’t the same calculation if one is VAT and the other is a markup — they’ll diverge the moment the VAT rate changes, and your abstraction will fight you every step of the way. DRY was always about not duplicating knowledge, a single fact that lives in one place. It was never about deleting code that happens to rhyme.
The wrong abstraction costs more than duplication
Sandi Metz put it more sharply than I can: prefer duplication over the wrong abstraction. Duplication is cheap to live with and cheap to fix — when you finally see the real pattern, you gather the copies and abstract them properly, and the cost was only ever a bit of typing. The wrong abstraction is the opposite. It couples things that shouldn’t be coupled, and unwinding it means unpicking every caller that came to depend on it, often across a boundary you no longer control.
That asymmetry is the whole argument. Get duplication wrong and you pay a small, linear price — a change made in two places instead of one. Get the abstraction wrong and you pay a compounding one, because every new requirement gets bent to fit a shape that was never right, and each bend makes the next person less able to tell what the thing was for. Duplication is a debt you can clear whenever you like. The wrong abstraction is a mortgage you took out on someone else’s house.
An abstraction is a door that’s harder to walk back through
I’ve written before about one-way and two-way doors — and a premature abstraction is a door that’s heavier than it looks. Introducing one is easy; it feels like progress. But once code depends on it, that abstraction becomes a small internal contract, and contracts other things build on are exactly the kind of decision that stops being cheap to reverse. You can absolutely refactor your way back out, but the cost grows with every caller, which is precisely the trajectory you want to avoid for a call you made early, on a hunch, at the point you understood the problem least.
Duplication, by contrast, keeps the door swinging both ways. Leaving two copies side by side costs you almost nothing and commits you to nothing — you can merge them the day the real abstraction reveals itself, and not a day sooner. Given the choice between a reversible bit of repetition and a hard-to-reverse bit of structure, the repetition is usually the more conservative bet.
Wait for the third case
The practical rule most teams converge on is the rule of three — write it once, copy it a second time and wince, and only on the third occurrence do you have enough evidence to abstract. Two data points can’t tell you which parts genuinely vary and which are fixed; three usually can. This isn’t a licence to be sloppy — it’s a way of buying information before you spend it. By the third call site the shape of the real abstraction is often obvious, and the version you write then is the one you’d have wished for if you’d guessed on Monday.
Kent C. Dodds has a nice reframing of this: avoid hasty abstractions — AHA — the idea being that you should be far more afraid of the wrong abstraction than of a little duplication. The point isn’t never to abstract. It’s to let the duplication sit long enough to tell you something first.
What you’re actually optimising for
This connects straight back to what you’re optimising for. Abstracting early optimises for the appearance of tidiness — a codebase with no visible repetition, which feels like craftsmanship. But the thing worth optimising for is usually change-friendliness: how cheap the next change is. And a premature abstraction actively works against that, because it turns “change one caller” into “change the shared thing and re-verify all of them.” You bought tidiness and paid in coupling, which is very nearly the worst trade in the catalogue — you took on the cost of an abstraction without the benefit of a correct one.
None of this is a defence of copy-paste-forever. Real duplication of knowledge — the same business rule implemented three subtly different ways — is a genuine hazard, and DRY is right to warn against it. The skill is telling that apart from code that merely looks similar today. When you’re not sure which one you’re looking at, the cheaper mistake is almost always to wait. You can always abstract duplication later. Clawing back the wrong abstraction is the expensive direction, and it’s the one the reflex pushes you towards.