The Wrong Abstraction
There’s a moment in every codebase where someone looks at two similar blocks of code and says, “We should extract this into a shared function.” The instinct is good. The execution is where things go wrong.
The DRY Trap
Don’t Repeat Yourself is one of the first principles engineers learn, and one of the most frequently misapplied. DRY is about knowledge, not text. Two blocks of code can look identical and represent completely different concepts. Merging them into one function creates a coupling between things that should evolve independently.
Consider two API endpoints that both validate an email address. Today, they use the same regex. But one is for user registration and the other is for newsletter signup. The validation rules for these might diverge – registration might need to check against a blocklist, newsletter signup might accept less conventional formats. If you’ve extracted the validation into a shared function, that divergence becomes a problem. You start adding parameters, flags, and special cases to handle both callers. The “shared” function becomes a negotiation between two different requirements.
The duplication was never the problem. The premature coupling was.
How Wrong Abstractions Grow
A wrong abstraction rarely starts wrong. It starts as a clean extraction of genuinely shared logic. Then the requirements diverge. Caller A needs a slight variation. Instead of inlining the logic back into caller A, someone adds a parameter. Then caller B needs a different variation. Another parameter. Then caller C uses the function but only needs half of what it does, so a flag is added to skip the other half.
Six months later, the function has eight parameters, three boolean flags, and a comment that says “WARNING: do not change this without checking all callers.” It’s the most coupled piece of code in the system. Everyone is afraid to touch it. Changes require understanding every caller’s specific needs and verifying that a fix for one doesn’t break another.
This is worse than the duplication ever was. Duplicated code is independent. You can change one copy without worrying about the other. A wrong abstraction is a shared dependency that resists change.
Recognizing the Signs
A few indicators that an abstraction has gone wrong:
The function signature keeps growing. If you’re adding parameters to handle new use cases, the abstraction isn’t capturing a stable concept. It’s becoming a switchboard.
You’re passing booleans to control behavior. A function that does different things based on a flag is two functions wearing a trench coat. Let them be separate.
Changes to one caller require careful analysis of all callers. This is the hallmark of bad coupling. Shared code should be stable. If it’s constantly being adjusted to accommodate different needs, it’s not shared – it’s contested.
The function name is vague. processData, handleRequest, doValidation. When you can’t name the abstraction clearly, it probably doesn’t represent a coherent concept.
The Inline Reflex
The fix for a wrong abstraction is often the scariest one: inline the code back into its callers. Undo the extraction. Duplicate it. Then let each copy evolve independently toward what it actually needs to be.
This feels like going backward. It feels like admitting a mistake. It is admitting a mistake, and that’s fine. The original extraction was a reasonable hypothesis about shared behavior. The hypothesis was wrong. Now you have new information. Use it.
Once the code is inlined, something interesting usually happens. The callers diverge quickly, confirming that they were never really doing the same thing. The “duplication” you were worried about turns out to be superficial – the underlying logic is different in ways that matter.
And sometimes, after inlining and letting things evolve, a new abstraction emerges. A real one, based on actual shared behavior rather than surface similarity. That abstraction is worth extracting.
When Abstraction Is Right
Good abstractions exist, and they’re worth fighting for. The difference is that good abstractions are discovered, not imposed. They emerge after you’ve seen the pattern repeat enough times to be confident it’s real.
A good abstraction has a clear name that describes exactly what it does. It has a stable interface that doesn’t need new parameters every month. Its callers use it in the same way, for the same reasons. Changing it benefits all callers equally.
If your abstraction doesn’t meet these criteria, it might be serving you better as duplicated code. That’s not a failure. That’s engineering judgment.
Duplicate until you’re sure. Then abstract.