There’s a piece of programming wisdom that refuses to die — and for good reason. Back in 2014, software developer and educator Sandi Metz dropped a line at RailsConf that split the room: ‘Duplication is far cheaper than the wrong abstraction.’ Nearly a decade later, that idea still circulates in engineering Slack channels, design reviews, and late-night debugging sessions. Understanding why the wrong abstraction costs so much — and how to escape it — is one of the most practically useful things a working developer can learn.
- The wrong abstraction accumulates hidden costs as developers keep patching it with parameters and conditionals instead of starting over.
- Sandi Metz argues that code duplication is far cheaper than the wrong abstraction, a claim that provoked strong reactions in the dev community.
- The sunk cost fallacy causes engineers to over-invest in broken code simply because it looks complex and hard-won.
- The fastest path out of a wrong abstraction is to inline the code back into callers, strip what’s unnecessary, and rebuild from scratch.
Table of Contents
How the Wrong Abstraction Is Born
It starts innocently enough. A developer — call them Programmer A — notices the same block of logic appearing in two or three places. They do exactly what they’ve been taught: they extract it, give it a name, and replace the repetition with a clean function or class. The code looks better. The duplication is gone. Programmer A moves on, satisfied.
Then time passes. A new requirement shows up that’s almost covered by the abstraction — but not quite. Programmer B arrives on the scene, sees the shared code, and feels what Metz describes as a kind of honour-bound obligation to keep it intact. So instead of questioning whether the abstraction still fits, they add a parameter. Then a conditional. The function now does subtly different things depending on what you pass in. Then Programmer C does the same. And Programmer X after that.
What started as a clean extraction has become a branching procedure stuffed with loosely related logic, conditional paths, and implicit assumptions about how each caller behaves. Nobody fully understands it anymore, but everyone’s afraid to touch it.

The Sunk Cost Trap That Keeps Developers Stuck
Here’s where the wrong abstraction becomes genuinely dangerous. As Metz points out, complex code exerts a kind of psychological gravity. The more tangled and incomprehensible something is, the more it feels important — as if someone spent enormous effort getting it exactly right, and dismantling it would waste all of that work. This is the sunk cost fallacy, and it’s just as common in software engineering as it is in business strategy or personal finance.
‘Goodness, that’s so confusing, it must have taken ages to get right,’ is how Metz describes the unconscious reasoning. ‘Surely it’s really, really important. It would be a sin to let all that effort go to waste.’ That internal narrative is what keeps developers pushing forward — adding yet another conditional, shipping yet another workaround — when the right move is to step back entirely.
The result is a codebase where adding new features becomes increasingly painful. Each change risks breaking something else. Each success makes the next task harder. Teams can spend weeks wrestling with a shared utility function that, at its core, probably shouldn’t exist in that form at all.
Why the Wrong Abstraction Beats Duplication for Damage
The intuition most developers carry is that duplication is the enemy. It’s one of the first lessons taught — Don’t Repeat Yourself, or DRY, is practically a religious tenet in software circles. But Metz’s point isn’t that duplication is good. It’s that the wrong abstraction is actively worse.
Duplicated code is at least transparent. Each copy is self-contained, understandable in isolation, and can be changed without touching anything else. A wrong abstraction, by contrast, hides complexity behind a shared name. Every caller is invisibly coupled to every other caller’s behaviour. Modify one path through the logic and you may inadvertently break three others. The more the abstraction has drifted from its original purpose, the harder it is to reason about what any particular caller is actually executing.
This is why Metz’s provocation landed with such force when she first said it. Developers who had spent months fighting a bloated shared module immediately recognised the pattern. The reaction on Twitter at the time was telling: many engineers responded with something close to relief at having the problem named clearly.

The Right Way to Escape a Wrong Abstraction
Metz’s prescription is counterintuitive but effective. When you’ve identified that you’re working with a wrong abstraction — typically the moment you find yourself adding yet another parameter or another conditional branch — the fastest path forward is backwards.
The process looks like this:
- Inline the abstraction. Take the shared code and paste it back into every single caller. Yes, every one. Embrace the temporary duplication.
- Trim each caller. Look at the parameters each caller was passing in. Use those to determine which subset of the inlined logic actually applies to that specific context. Delete everything else.
- Start fresh. Once each caller contains only the code it genuinely needs, step back and look at what you have. Real patterns — if any exist — will now be visible without the noise of historical decisions obscuring them.
What Metz found, working through real codebases with teams, is that this process often reveals something surprising: the callers that appeared to be sharing logic were actually doing quite different things. The abstraction had never been as universal as it looked. Once the wrong abstraction is removed and the duplication is exposed honestly, the correct structure tends to become obvious on its own.
What This Means for How Teams Build Software
There’s a broader lesson here about how engineering teams think about code ownership and investment. The instinct to preserve existing code — especially code that looks hard-won — is deeply human. But treating a function or class as a permanent fixture, rather than a hypothesis about structure that can be revised, is what turns manageable technical debt into a genuine productivity crisis.
The wrong abstraction problem is also a communication problem. When Programmer B arrives and sees a shared utility, there’s often no signal that its original design is already under stress. The code compiles. The tests pass. The abstraction looks authoritative. Nothing in the environment communicates ‘this was a good idea in 2019 but we’ve since learned it doesn’t generalise.’ That kind of institutional memory lives in people, not in code — and people leave.
This is part of why practices like thorough code review, living architecture documentation, and genuinely honest retrospectives matter. They create space for the conversation that Metz is describing: ‘This code made sense for a while, but perhaps we’ve learned all we can from it.’ That reframe — from ‘preserve our investment’ to ‘what does the code need to be now’ — is the actual shift required.
Metz’s core point has only become more relevant as codebases get larger and teams more distributed. In a world where AI coding assistants are generating abstractions at speed, the risk of accruing wrong ones faster than ever is real. The ability to recognise when an abstraction has outlived its usefulness, resist the pull of sunk costs, and deliberately reintroduce duplication as a step toward clarity — that’s a skill worth sharpening, no matter what’s writing the first draft of your code.
Source: Hacker News
Frequently Asked Questions
What is a wrong abstraction in software development?
A wrong abstraction occurs when shared code that once represented a clean, universal concept gets stretched to handle too many different cases. As new requirements pile on, developers add parameters and conditionals until the original abstraction no longer reflects any single coherent idea, making the code brittle and hard to follow.
Is code duplication ever better than abstraction?
According to Sandi Metz, yes — duplication is far cheaper than a wrong abstraction. Duplicated code is at least transparent and self-contained. An incorrectly abstracted function that branches across a dozen use cases is far harder to understand, modify, and test without breaking something else.
How do you fix a wrong abstraction in an existing codebase?
Metz recommends reversing course: inline the abstracted code back into every caller, use the parameters being passed to identify what each caller actually needs, delete the rest, and only then look for genuine new abstractions that reflect current requirements rather than historical assumptions.
Why do developers keep using the wrong abstraction instead of rewriting it?
The sunk cost fallacy is the main culprit. Complex, convoluted code gives the impression of deep effort and importance. Developers feel a psychological obligation to preserve that investment, even when pushing forward makes every subsequent feature harder to ship.

