Back to blog

The Wrong Abstraction: Why Duplication Is Better Than Perfect Code

One innocent refactoring, five parameters, and a decade of technical debt. How the wrong abstraction eats your codebase alive.

Privacy & Governance Software EngineeringRefactoringArchitecture

Code abstraction vs duplication dilemma illustrated

There is a moment in a programmer’s work that is not customary to discuss out loud, but which is familiar to anyone who has ever refactored someone else’s code. You look at a method that has been living in the codebase for three years. It used to have a simple name, but now it has five parameters, three of which are optional. Inside there is a cascade of conditions, each handling a separate case. The code works, the tests pass, but you feel that something is broken. Not in the code. In the architecture.

And this is sad, because it all started right. Someone noticed duplication, two identical pieces of code in different places. Collected them into one method, gave it a name. Clean, short, clear. But then a new requirement came along, almost perfectly fitting this abstraction, but not quite. And someone added a parameter. Then another one. Then a condition: if the flag is True, do this, if False, do that. The abstraction ceased to be general. It became a procedure pretending to be an abstraction.

The most insidious thing is not the code itself. The most insidious thing is our attitude toward it. The more effort invested, the harder it is to admit that this effort was directed the wrong way. In psychology, this is called the sunk cost fallacy. In programming, it sounds like this: “this method is so complex, people worked on it for months, you cannot throw it away.” You can and you should. Because code complexity is not proof of its value. Quite the opposite. If you need three pages to explain what one method does, the abstraction is dead and you are burying it.

The only way out of this trap is to admit that this abstraction’s time has passed. Do not fix it, do not prop it up with crutches, do not add one more parameter. Unwind it back. Inline the code back into each calling method, remove the unnecessary pieces, leave each specific case with exactly what it needs. This sounds like a step back. In reality, it is a step forward, just in a different direction.

When you do this for the first time, you discover an amazing thing. It turns out that the originally “identical” pieces of code were actually different. They just seemed similar because you looked at them from a distance. And when you inline them back, you see the unique logic of each case. And right there it becomes clear what abstraction should have been built instead.

I have noticed this many times in real projects. A team struggles with a new feature for weeks because the existing abstraction does not fit, but it feels wrong to change it. The code becomes increasingly strange: flags appear, fallback logic, magic numbers. And then someone comes in and does an inline, and the feature ships in two days. Not because that someone is a genius. But because they are not afraid to throw away what stopped working. The irony is that in programming, the ability to abandon your own code is often valued more than the ability to write it.

Duplication is not the enemy. The enemy is premature correctness. Code that tries to be universal before all the use cases have revealed themselves. Better to have three similar pieces that are easy to read and change independently, than one abstraction that truly fits none of them.

Good architecture is not the one where there is no duplication. Good architecture is the one where every abstraction honestly reflects what we know about the task right now. And not what we assumed a year ago.

More thinking