We learn DRY early: do not repeat yourself. So we extract, generalize, and parameterize. And sometimes that is exactly right.
But a principle is not a rule. DRY tells you duplication has a cost. It does not tell you abstraction is free. Every abstraction has a cost too, and the wrong one costs more than the duplication it removed.
Duplication is visible. A wrong abstraction is not.
Duplicated code is easy to see, easy to find, and easy to delete later. Two similar functions sitting side by side are honest about what they are. You can read each one on its own and understand it.
A shared helper with six flags is a different story. Nobody dares touch it, because changing one flag's behavior might break five callers nobody remembers. The duplication is gone, but the complexity moved somewhere harder to see: into the branching, the parameter combinations, and the mental model you need before you can change anything.
That is the trade most DRY discussions skip. Duplication costs you keystrokes and a wider edit when something changes. A premature abstraction costs you understanding, and understanding is the more expensive currency.
When duplication is the better choice
Duplicate first when the two copies only look alike. Similar code is not the same code. Two blocks that happen to do the same thing today, for different reasons, will change for different reasons tomorrow. Merge them now and the next change has to fight the abstraction: a new flag, a special case, an if statement that exists only because two callers disagree.
Duplicate first when you do not yet know what is shared. The first time you write something, you are guessing at the shape of it. The second time, you have a comparison. It is usually the third time that the real pattern shows itself, the part that is genuinely shared rather than coincidentally similar.
Duplicate first when the copies are small and stable. Three lines repeated in two places is not a maintenance crisis. It is three lines. The abstraction that replaces it can easily be longer than the duplication it removed, and longer to understand.
When abstraction earns itself
Abstraction earns itself when the shared part is stable and the differences are explicit. A good abstraction has few parameters, a clear name, and callers that all want the same thing from it. If you can describe what it does in one sentence without saying "except when," it is probably ready.
It also earns itself when the cost of getting it wrong is high. A validation rule, a security check, a calculation that must agree everywhere: that is one idea, and it should live in one place. Duplicating that is not pragmatism, it is a bug waiting for one copy to drift.
The difference is whether the copies share a reason to change. If they change together, for the same reason, they are one idea wearing two costumes, and one place is right. If they change separately, for different reasons, they are two ideas that happen to look alike, and merging them creates a coupling that was not there before.
A test that actually helps
Before extracting, ask one question: what changes next?
If the answer is "both copies, in the same way, for the same reason," extract. If the answer is "one of them, and I am not sure which," wait. Write the duplicate, let the pattern earn its abstraction, and revisit it when the third copy arrives. By then you will know what is actually shared, because you will have seen it change.
That is the version of DRY worth keeping: do not repeat knowledge, not do not repeat text. Two copies of the same text are a smell. Two copies of the same idea, kept in sync by discipline, are a choice. And a choice you can revisit is cheaper than an abstraction you cannot unwind.
Your turn
Where do you stand: abstract early, or duplicate first? And what is the worst premature abstraction you have had to unwind, the helper with too many flags, the base class that fit nothing quite right? What made it so hard to undo?
I would rather hear one specific story than a general opinion. Which abstraction, and what did it cost?
Top comments (0)