The DRY Principle: Don’t Repeat Yourself in

The DRY Principle: Don’t Repeat Yourself in Software Development

Every experienced developer has been there. You fix a bug in one place, ship it, and two weeks later the same bug appears in a different part of the system. Same logic. Different file. Nobody noticed they were duplicated.

That is the DRY principle problem in practice.

DRY stands for Don’t Repeat Yourself. Andy Hunt and Dave Thomas introduced it in their 1999 book “The Pragmatic Programmer,” defining it this way: every piece of knowledge must have a single, unambiguous, authoritative representation within a system. It is not just about avoiding copy-paste. It is about making sure that any given concept, rule, or piece of logic lives in exactly one place.

What DRY Actually Means

The word “knowledge” in that definition is deliberate. DRY is broader than “don’t write the same code twice.” Configuration values duplicated across files violate DRY. Business rules that appear in both the backend API and the frontend validation violate DRY. Database schema that is manually re-described in a data access layer violates DRY.

When something needs to change, you should make that change in one place. If you have to find and update three, five, or ten locations, you have a DRY violation. Those are the bugs waiting to happen.

The opposite of DRY is sometimes called WET: Write Everything Twice. Or as Hunt and Thomas put it, “We Enjoy Typing.” A WET codebase is fragile. It resists change. Every modification becomes a search operation, and human beings miss things in searches.

Real-World Situations Where DRY Problems Appear

Picture an e-commerce platform. Discount logic calculates a customer’s price reduction based on their tier and order history. That logic gets written in the checkout service when it is first built. Six months later, someone adds a price preview feature and writes the same discount calculation in a second service. A year after that, the discount rules change. One team updates the checkout service. Nobody knows about the preview service. Customers see one price in the preview and pay a different amount at checkout. Support tickets pile up.

This is a DRY failure that produces real business damage.

Validation is another common site. A phone number format check written in a registration form, then written again in the profile edit form, then written again in an admin import tool. When the format requirement changes, whoever updates one of them assumes the others will follow. They often do not.

How to Resolve Duplication

Recognizing WET code is one thing. Fixing it requires judgment about the right approach for the situation.

The most common fix is extracting a function or method. If the same logic appears in multiple places, pull it into a named function with a clear responsibility. Both callers use the function. Now there is one place to update.

For shared behavior across classes, extracting a base class or service works when the duplication spans multiple types with a genuine relationship. A base repository class in C# that handles common database operations is a reasonable extraction. A utility class for string formatting that gets imported by twenty different modules is another.

Generics and type parameters handle duplication across different data types. Instead of writing a sorting function for integers and a separate one for strings, one generic function handles both. TypeScript and C# both make this straightforward.

Configuration constants deserve special attention. Magic numbers — tax rates, time limits, default page sizes — should live in a single configuration location, not be scattered as literals throughout the code.

Code Examples

C# — Before and After

Before: discount logic duplicated across two services.

public class CheckoutService
{
    public decimal GetDiscountedPrice(decimal price, CustomerTier tier)
    {
        if (tier == CustomerTier.Gold) return price * 0.85m;
        if (tier == CustomerTier.Silver) return price * 0.90m;
        return price;
    }
}

public class PricePreviewService
{
    public decimal GetDiscountedPrice(decimal price, CustomerTier tier)
    {
        if (tier == CustomerTier.Gold) return price * 0.85m;  // Duplicated!
        if (tier == CustomerTier.Silver) return price * 0.90m;
        return price;
    }
}

After: a single authoritative source for discount logic.

public static class DiscountCalculator
{
    private static readonly Dictionary<CustomerTier, decimal> Rates = new()
    {
        { CustomerTier.Gold, 0.85m },
        { CustomerTier.Silver, 0.90m },
    };

    public static decimal Apply(decimal price, CustomerTier tier)
        => Rates.TryGetValue(tier, out var rate) ? price * rate : price;
}

public class CheckoutService
{
    public decimal GetDiscountedPrice(decimal price, CustomerTier tier)
        => DiscountCalculator.Apply(price, tier);
}

public class PricePreviewService
{
    public decimal GetDiscountedPrice(decimal price, CustomerTier tier)
        => DiscountCalculator.Apply(price, tier);
}

Now when discount rules change, one class changes. Both services get the update automatically.

TypeScript — Before and After

Before: validation logic repeated across feature modules.

// In registration.ts
function isValidEmail(email: string): boolean {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
function isValidPhone(phone: string): boolean {
  return /^\d{10}$/.test(phone);
}

// In profile.ts — identical functions, different file
function isValidEmail(email: string): boolean {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
function isValidPhone(phone: string): boolean {
  return /^\d{10}$/.test(phone);
}

After: a shared validators module used by all features.

// validators.ts — single source of truth
export const validators = {
  email: (value: string): boolean =>
    /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
  phone: (value: string): boolean =>
    /^\d{10}$/.test(value),
};

// registration.ts
import { validators } from '../shared/validators';
if (!validators.email(input.email)) throw new Error('Invalid email');

// profile.ts
import { validators } from '../shared/validators';
if (!validators.phone(input.phone)) throw new Error('Invalid phone');

One file holds the validation rules. Any feature that needs them imports from the same place.

DRY and the SOLID Principles

DRY does not exist in isolation. It works alongside SOLID, and the relationship is closest with the Single Responsibility Principle.

SRP says a class should have exactly one reason to change. When you extract duplicated discount logic into a DiscountCalculator class, you are doing both at once. You are eliminating duplication and you are giving that class a single clear responsibility: apply discount rules. The CheckoutService is no longer responsible for knowing discount rates. The PricePreviewService is not either. Each class has one reason to change. When discount rules change, only DiscountCalculator changes.

This compound effect is why applying DRY often improves a codebase in multiple dimensions simultaneously. You get reduced duplication, clearer responsibilities, easier testing, and better separation of concerns in one move.

DRY also supports the Open/Closed Principle. Code that centralizes shared logic can be extended with new behaviors without requiring modifications scattered across multiple files. A single pricing engine can accept new discount strategies through a strategy pattern without touching any of the callers.

The Dependency Inversion Principle connects to DRY through abstraction. When high-level modules depend on abstractions rather than concrete implementations, the specific knowledge of how something works lives in one place — the implementation — rather than being duplicated across every caller.

When Not to Apply DRY

DRY has a failure mode: premature abstraction.

Sandi Metz described it well: the wrong abstraction is worse than duplication. When you find two pieces of code that look similar, the temptation is to immediately extract them into a shared function. But similar code is not always the same code. Two registration flows that share a few validation checks might diverge significantly over the next six months as different product requirements hit them. Premature extraction creates a tangled shared function that grows harder to modify as both callers pull it in different directions.

AHA — Avoid Hasty Abstractions — is the counterweight to DRY. Let duplication sit for a while. If the pattern becomes clear and stable, extract it. If the two code paths diverge, keep them separate.

A useful rule: wait until the same knowledge appears in three places before abstracting. Two occurrences might be coincidence. Three is a pattern.

Further Reading

Want help applying DRY and SOLID principles to your team’s codebase? Let’s talk.

The DRY Principle: Don’t Repeat Yourself in Software Development

Leave a Reply

Your email address will not be published. Required fields are marked *