One time I deleted what seemed to be a duplicated file, but it reappeared. I deleted it again, and the same thing happened. I did this three times before realizing it was an important file that kept regenerating itself for some reason. 🙃
The reflex we all learned wrong
At some point in your initial year, you received the same advice that I did. If you see two similar blocks, separate them. Right away. Doing things the DRY (Don't Repeat Yourself) way has been ingrained so deeply in our culture for so long that it became a knee-jerk reaction. Repetition is bad; software is about handling complexity, and removing repetition is just part of that. However, it's not actually part of that - it's a side effect we now mistake for a cause. Here's the catch. It is more difficult to eliminate the wrong abstraction than the redundancy it substituted.
Sandi Metz called it a decade ago
The phrase was coined by Sandi Metz during her RailsConf 2014 talk "All the Little Things." In no uncertain terms, she expressed that duplication is far cheaper than the wrong abstraction.
In January 2016, she elaborated on this in a blog post titled "The Wrong Abstraction" and her explanation is quite powerful. A bad abstraction turns into a "condition-laden process that interweaves a handful of loosely related concepts." That function with eight boolean flags that no one dares refactor. The part of her advice that everyone skips is the most important. "If the abstraction is wrong, the fastest way to get to the right abstraction is to go back to duplicating."
Why the abstraction fights back
Duplicated code is dumb and honest. The incorrect abstraction is not that. Each new caller adds another parameter, another 'if', another special case. It sprouts teeth. And no one wants to be in the position to delete it. Hence, it stays there, accumulating flags similar to barnacles. → Duplication has a local blast radius. One change, one place. → A bad shared function has a global one. One change, ten callers you didn't know existed. → Un-inlining a tangled abstraction is real archaeology. Copy-pasting is a keystroke.
The pragmatists agree
It was Rob Pike, Go co-creator, back at Gopherfest SV in November 2015 who included it in the Go Proverbs: "A little copying is better than a little dependency."
Consider what a dependency really is. It's a promise that two things will keep meaning the same thing forever. That promise is broken all the time. Kent C. Dodds elaborated on the same concept with "AHA Programming" - Avoid Hasty Abstractions, terminology first coined by Cher Scarlett. His mantra is just perfect. Keep replicating the code until the precise abstraction becomes evident through the repetition. Should you identify a pattern, don't assume it's the correct one after the second repetition. Revisit this process for a third or fourth repetition and let the pattern naturally emerge.
It's not just taste, it's productivity
Here is where I cease gesticulating wildly. Daniel J. Sturtevant's 2013 MIT PhD thesis quantified the impact of complex architecture on actual teams. Working in high-complexity areas dropped developer productivity by up to 50%. Bug density tripled. 📉
Also, turnover among employees increased significantly. Individuals will leave a certain codebase because they are unable to understand it. The process of prematurely implementing an abstraction contributes to this.
What I actually do now
In our team, at a small startup, I ceased extracting on sight. If two blocks resemble each other, I let them be. I will wait and see. If they mutate simultaneously three times, let's say they are one thought and I will merge them. If they mutate separately, I've avoided a landmine. The key is to be patient, and patience doesn't feel right when you have been accustomed to feeling smart. Being lazy for a few lines of code by copy-pasting I have learned to digest that. 😅
Because the alternative is a "clever" helper that ten people curse for two years.
The takeaway
It's not that DRY is incorrect. It's more like DRY came into the game too soon. The real mistake is not in repeating yourself, but in making assumptions about the abstraction you're creating before your code clarifies what it is. Wait for the scream, and then take out the core. Here's a question for you: Can you think of the worst abstraction you were ever hesitant to remove, and if so, did you ultimately remove it or did you keep it in place?
Top comments (1)
The line I use now is the rule of three, but with a twist: the third occurrence gets an abstraction only if I can describe its contract in one sentence without saying "and also". Every time I extracted something whose signature needed a boolean for each caller's edge case, I was hiding a branch, not removing duplication.
Where this bit me recently is agent-generated code: models love factoring out a helper on the first or second repetition because it looks tidy in the diff, and the helper is always subtly wrong for the third caller. I now explicitly tell the model to duplicate twice before extracting. Has your tolerance changed since working with LLM-written code, or do you hold the same bar as with human PRs?