Skip to content
Dev Tools Article

Why Code Duplication Beats the Wrong Abstraction

Prematurely drying up your codebase couples independent business domains, creating a maintenance nightmare far worse than copy-paste.

Lenn Voss
Lenn Voss
Cloud & Infrastructure Writer · Jun 21, 2026 · 6 min read
Why Code Duplication Beats the Wrong Abstraction

Every junior developer learns the DRY (Don't Repeat Yourself) principle as if it were handed down on stone tablets. We are taught to hunt down duplication like a compiler error, abstracting away repetition at the very first sign of syntactic similarity.

But dogmatic adherence to DRY has a dark side. In the rush to eliminate duplicate lines of code, developers frequently build premature abstractions that couple independent parts of a system. Years ago, Ruby pioneer Sandi Metz popularized a counter-cultural thesis on her blog: "duplication is far cheaper than the wrong abstraction."

This is not just a clever catchphrase; it is a fundamental truth of software architecture. When you abstract too early, you create a rigid structure based on what you think the pattern is, rather than what it actually becomes. Over time, this premature structure becomes an architectural trap that is incredibly expensive to escape.

The Anatomy of a Decaying Abstraction

The descent into a bad abstraction almost always follows a predictable, slow-motion pattern. It starts innocently. A developer notices two separate modules performing similar operations—for instance, fetching user data from a database.

To eliminate this duplication, they extract the common logic into a single, shared function:

// The "clean" shared abstraction
function getUser(userId, purpose) {
  let fields;
  if (purpose === 'authentication') {
    fields = 'id, username, password_hash, login_attempts';
  } else if (purpose === 'profile') {
    fields = 'id, username, full_name, bio, avatar_url';
  }
  return database.query(`SELECT ${fields} FROM users WHERE id = ${userId}`);
}

At first glance, this looks like a win. Duplication is gone, and the code feels tidy. But look closer: this function now has multiple responsibilities, violating the Single Responsibility Principle. It forces the caller to pass a purpose parameter, leaking implementation details and tightly coupling two entirely different business use cases (authentication and profile display).

As requirements evolve, the decay accelerates. A new requirement demands that the profile display include a user's active status, but authentication does not need it. Another developer adds a third conditional path. Six months later, a different developer needs to handle contractor accounts differently than employee accounts, adding a fourth parameter and another nested if statement.

This is "death by a thousand parameters." What was once a simple utility has morphed into a complex, condition-laden procedure that interleaves vaguely associated ideas. It is hard to read, terrifying to modify, and incredibly easy to break.

Syntactic vs. Semantic Duplication

The core mistake behind premature abstraction is failing to distinguish between syntactic duplication (code that looks the same) and semantic duplication (code that represents the same underlying business concept).

Just because two blocks of code share the same structure today does not mean they share the same destiny. Consider a system calculating bonuses for employees and contractors. Initially, both calculations might look identical: a base rate multiplied by a performance factor.

If you abstract this into a shared calculateBonus() function, you are betting that employee and contractor bonus structures will evolve in lockstep. But they won't. Employees will eventually get tenure bonuses; contractors will get prorated calculations based on billable hours.

If you coupled them early, you are forced to choose between two bad options:

  1. Hack the shared abstraction with conditional flags and parameters.
  2. Undertake a risky refactoring of a core system.

If you had left the duplicated code alone, changing the employee bonus logic would have been a trivial, isolated edit. The duplication was merely syntactic; semantically, they were entirely different concepts.

The Counter-Argument: When Duplication is Dearer

While premature abstraction is dangerous, blind duplication is not a free lunch. Critics of the "duplication is cheaper" mantra rightly point out that massive, untracked duplication introduces its own severe maintenance costs.

If a critical bug is found in a duplicated block of code—such as a flawed validation regex or an insecure parsing routine—and that block is copy-pasted across dozens of files, the fix must be manually applied to every instance. Miss one, and you introduce an inconsistency that can lead to security vulnerabilities or subtle runtime bugs.

Furthermore, massive duplication increases cognitive load. A developer trying to understand a legacy C++ codebase, for example, can easily get lost in a sea of duplicated classes (Foo, Foo2, FooX) created to handle minor variations of the same calculation.

Therefore, the goal is not to abandon DRY entirely, but to apply it pragmatically.

The Developer's Playbook: How to Choose

To navigate this trade-off, developers need clear, actionable heuristics to determine when to abstract and when to let duplication stand.

flowchart TD
    A[Identify Duplicated Code] --> B{Is it Domain or Utility?}
    B -->|Utility| C[Abstract Immediately]
    B -->|Domain| D{Are there 3+ instances?}
    D -->|No| E[Keep Duplicated / Wait]
    D -->|Yes| F{Do they share the same business concept?}
    F -->|Yes| G[Abstract]
    F -->|No| E

1. The Utility vs. Domain Split

  • Abstract Utilities Early: Pure technical utilities—such as date formatters, email validation regexes, and JSON parsers—have narrow scopes and clear purposes. They rarely change based on shifting business requirements. Abstract these immediately.
  • Delay Domain Logic: Business rules, database queries, and UI workflows are highly volatile. Keep them duplicated until their evolutionary paths are clear.

2. The Rule of Three (or Four)

Do not abstract when you see the pattern twice. Two points always form a straight line, but that line might point in the wrong direction. Wait until you have three or four concrete implementations. By then, you will have real data on what actually varies and what remains constant, allowing you to design an abstraction that fits the actual problem rather than an imagined one.

3. The Escape Hatch: How to Unwind a Bad Abstraction

If you inherit a codebase trapped in a maze of conditional abstractions, do not try to push forward. The fastest way forward is back.

Use Sandi Metz's recipe to unwind the damage:

  1. Inline the code: Copy the abstracted code back into every caller.
  2. Clean up the callers: Within each caller, evaluate the parameters passed to the old abstraction. Delete any conditional branches that are not executed by that specific caller.
  3. Let it breathe: Now that the callers are isolated and simple again, let the duplication exist until a clean, genuine pattern emerges.

The Sunk Cost Trap

Existing code exerts a powerful psychological gravity. Because an abstraction took time and effort to build, developers feel a strong urge to preserve it, even when it clearly no longer fits. This is the sunk cost fallacy in action.

An abstraction is a hypothesis about the future. When that hypothesis is proven wrong, keeping it alive with parameters and conditionals is not clean coding—it is technical debt. Give yourself and your team permission to delete the wrong abstraction, re-introduce duplication, and let the code tell you what it actually wants to be.

Sources & further reading

  1. Code duplication is far cheaper than the wrong abstraction — sandimetz.com
  2. Why I don't buy "duplication is cheaper than the wrong abstraction" - Code with Jason — codewithjason.com
  3. Duplication Is Not the Enemy – Terrible Software — terriblesoftware.org
  4. patterns and practices - Code duplication vs. abstraction - Software Engineering Stack Exchange — softwareengineering.stackexchange.com
  5. Why Your Code Duplication Isn’t Always Bad: A Pragmatic Approach to the DRY Principle – AlgoCademy Blog — algocademy.com
Lenn Voss
Written by
Lenn Voss · Cloud & Infrastructure Writer

Lenn writes about cloud platforms, Kubernetes internals, and the infrastructure decisions that quietly make or break engineering organizations. Based in Berlin's vibrant tech scene, they have a talent for turning dense platform-engineering topics into prose that people actually finish reading.

Discussion 0

Join the discussion

Sign in or create an account to comment and vote.

No comments yet

Be the first to weigh in.

Related Reading