# DRY Is the Most Misunderstood Rule in Programming

> DRY was never about code that looks the same. It's about knowledge. Most of the tangled abstractions I've had to unwind started as a well-meaning attempt to remove duplication that was never really duplication. Here's how I think about it now, and the rule I use instead.

Published: 2026-06-06. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/dry-is-the-most-misunderstood-rule-in-programming

The original DRY rule is about knowledge, not code that looks the same.
A fact like a tax rule should have one authoritative home, while two blocks that only resemble each other are separate concepts that will drift.
Extract on the third occurrence, and only when both copies must change together.

Every developer learns DRY early, and almost everyone learns it wrong.

Don't Repeat Yourself.
See two pieces of code that look the same, extract a method, delete the duplicate.
I did this for years and wrote some of the worst code I've ever had to maintain:

- A shared helper that grew a new boolean parameter every sprint.
- A base class nobody dared touch because six unrelated features inherited from it.
- A "common" module two independent parts of the system both depended on, so neither could change without the other.

Every one started as an innocent attempt to not repeat myself.

## What DRY Actually Says

Here's the part most people skip.
The original definition, from Andy Hunt and Dave Thomas in [_The Pragmatic Programmer_](https://en.wikipedia.org/wiki/The_Pragmatic_Programmer),
says nothing about code:

> Every piece of **knowledge** must have a single, unambiguous, authoritative representation within a system.

It's about knowledge. A single _fact_ about your domain, like a tax rule or the format of an invoice number,
should live in exactly one place.
When that fact changes, you change it once instead of hunting for seven copies.

## The Mistake: Deduplicating Code, Not Knowledge

Two pieces of code can look identical and represent completely different knowledge.

Say you validate two addresses.
One is a customer's shipping address, the other is a warehouse address.
Today the rules are identical:

```csharp
public bool IsValid(Address address) =>
    !string.IsNullOrWhiteSpace(address.Street) &&
    !string.IsNullOrWhiteSpace(address.City) &&
    !string.IsNullOrWhiteSpace(address.PostalCode);
```

The DRY reflex says extract one validator and call it from both places.
But these are different concepts that happen to share rules this week.
The day the warehouse needs a loading-dock code,
you're back in the shared method bolting on a flag to keep the other caller working:

```csharp
public bool IsValid(Address address, bool requireDockCode = false) =>
    !string.IsNullOrWhiteSpace(address.Street) &&
    !string.IsNullOrWhiteSpace(address.City) &&
    !string.IsNullOrWhiteSpace(address.PostalCode) &&
    (!requireDockCode || !string.IsNullOrWhiteSpace(address.DockCode));
```

That boolean is the tell.
The first time a shared method grows a flag so one caller behaves differently, you didn't have duplication.
You had two things that looked alike and glued them together.
Give it a year and the signature has three more flags,
each one a place where the two concepts were never actually the same.

## The Wrong Abstraction Costs More Than Duplication

**Duplication is far cheaper than the wrong abstraction**.

Copy-paste is visible and local.
You can see both copies, and if they drift apart, that was always allowed.
The wrong abstraction is invisible and global.
Every caller depends on one shape and bends it to fit, the flags pile up,
and you end up afraid to touch a method you no longer understand.
I've spent more time deleting bad abstractions than I ever saved writing them.

This is the [**hidden coupling cost I wrote about in the abstractions piece**](https://milanjovanovic.tech/blog/the-real-cost-of-abstractions-in-dotnet),
and DRY-by-reflex is one of the most common ways it sneaks in.

## Where It Hurts Most: Across Boundaries

Inside one class, a bad helper is annoying. Across module boundaries, it's structural damage.

Picture a [**modular monolith**](https://milanjovanovic.tech/blog/what-is-a-modular-monolith) with a `Billing` module and a `Shipping` module.
Both have an `Order`.
A well-meaning engineer notices the two classes share fields and pulls them into a shared type both modules reference:

```csharp
// Shared.Orders, referenced by both Billing and Shipping
public class Order
{
    public Guid Id { get; set; }
    public string CustomerName { get; set; }
    public decimal Total { get; set; }
    // ...whatever either module happens to need
}
```

Now Billing and Shipping can't evolve independently.
A change to billing's order forces a recompile, re-test, and redeploy of shipping.
You took two [**bounded contexts**](https://milanjovanovic.tech/blog/bounded-context-ddd-explained) that were supposed to be decoupled and welded them together to save a few properties.

Two modules each owning their own `Order` is the whole point of [**keeping data inside its boundaries**](https://milanjovanovic.tech/blog/how-to-keep-your-data-boundaries-intact-in-a-modular-monolith). The shapes are allowed to be similar, modeling the same real-world thing from two points of view that drift over time. It's the same reason [**vertical slices**](https://milanjovanovic.tech/blog/vertical-slice-architecture-is-easier-than-you-think)
tolerate a little repetition, so each slice can change on its own.

## The Rule I Use: Wait for the Third Time

I don't deduplicate the second time I see something.
I wait for the third, and I ask one question: if this rule changes, do both copies have to change together?

- **Yes** - it's real duplication, the same fact written in several places. Extract it, and that's DRY doing its job.
- **No** - the resemblance is a coincidence. Leave it alone, and coupling them will cost you later.

Let the code repeat until the right abstraction becomes obvious,
because good abstractions are discovered from concrete cases, not guessed up front.
Some people call this AHA, for "Avoid Hasty Abstractions."

A practical tell: extract when you can name the concept.
A real domain name like `Money`, `TaxRate`, or `InvoiceNumber` is probably knowledge worth a [**value object**](https://milanjovanovic.tech/blog/value-objects-in-dotnet-ddd-fundamentals).
If the best name you can find is `Helper`, `Utils`, or `ProcessData`, you're abstracting shape, not knowledge.

## When DRY Is Right

Applied correctly, DRY is invaluable.
A business rule belongs in exactly one place.
Watch what happens when "an order over $1,000 needs manager approval" gets copy-pasted across three services:

```csharp
// OrderService
if (order.Total > 1000) { /* require approval */ }

// CheckoutService
if (order.Total > 1000m) { /* require approval */ }

// AdminController - someone bumped the limit here, and only here
if (order.Total > 5000) { /* require approval */ }
```

You will eventually update two of them and ship a bug.
That drifted third copy is exactly how it happens.
Push the rule into the [**domain model**](https://milanjovanovic.tech/blog/refactoring-from-an-anemic-domain-model-to-a-rich-domain-model) where it has one home:

```csharp
public bool RequiresManagerApproval() => Total > 1000;
```

That's the single authoritative representation DRY is actually about.

## Summary

- DRY is about **knowledge**, not code that looks alike.
- The wrong abstraction costs more than the duplication it replaced, and it's harder to undo.
- Wait for the third occurrence. Extract only when both copies encode the same fact and must change together.

The next time you're about to delete a duplicate, don't ask whether the code looks the same.
Ask whether it _means_ the same.
That one question will save you more maintenance pain than DRY ever saved you typing.

If you want to see how I draw these boundaries in a real system,
with independent modules and abstractions that earn their place, that's the heart of [**Pragmatic Clean Architecture**](https://milanjovanovic.tech/pragmatic-clean-architecture).

Thanks for reading.

And stay awesome!

---

## Frequently asked questions

### What does the DRY principle actually mean?

The original definition from Andy Hunt and Dave Thomas in The Pragmatic Programmer says every piece of knowledge must have a single, unambiguous, authoritative representation within a system. It is about domain facts like a tax rule, not about code that happens to look the same.

### Is code duplication better than the wrong abstraction?

Duplication is far cheaper than the wrong abstraction. Copy-paste is visible and local, and the copies are allowed to drift apart. A wrong abstraction is invisible and global: every caller bends it to fit, flags pile up, and nobody dares touch it.

### When should you extract duplicated code into a shared method?

Wait for the third occurrence, then ask one question: if this rule changes, must both copies change together? If yes, it is the same fact and worth extracting. If no, the resemblance is a coincidence and coupling the copies will cost you later.

### Why are boolean flag parameters a code smell?

The first time a shared method grows a flag so one caller behaves differently, you never had real duplication. You had two different concepts that looked alike and got glued together, and each new flag marks another place where they diverge.

### Should modules in a modular monolith share entity classes?

No. If Billing and Shipping share one Order type, a change for one module forces a recompile, re-test, and redeploy of the other. Each module should own its own model of the same real-world thing, letting the shapes drift independently over time.

### What is AHA (Avoid Hasty Abstractions)?

AHA means letting code repeat until the right abstraction becomes obvious, because good abstractions are discovered from concrete cases rather than guessed up front. A practical tell: extract when you can name the concept, like Money or TaxRate, not Helper or Utils.
