Code Quality Principle · DRY

Don't Repeat Yourself

Every piece of knowledge must have a single, authoritative representation.

DRY Principle: A Complete Guide with Examples in 11 Programming Languages

The DRY (Don't Repeat Yourself) principle states that every piece of knowledge or logic must have a single, unambiguous, authoritative representation within a system. It was introduced by Andy Hunt and Dave Thomas in The Pragmatic Programmer (1999) and remains one of the most foundational principles in software development. When knowledge is duplicated — in code, configuration, documentation, or database schemas — changes require updating every copy. Miss one, and you have a bug. The DRY principle eliminates this category of risk entirely by ensuring that every fact about your system lives in exactly one place.

In this comprehensive guide, we'll explore the DRY principle with real-world examples in Python, TypeScript, Java, JavaScript, C#, PHP, Go, Rust, Dart, Swift, and Kotlin — complete with language-specific best practices, common pitfalls, and when to apply this fundamental principle.

Table of Contents

What is the DRY Principle?

DRY is about knowledge, not just code. The full principle from The Pragmatic Programmer reads: "Every piece of knowledge must have a single, unambiguous, authoritative representation within a system." The word "knowledge" is deliberate — duplication of logic, data, intent, or meaning all violate DRY, even when the characters on screen differ.

The naive form of DRY violation is copy-paste: you write a function to validate an email address, then copy it into three other files because it's faster than importing it. Now you have four validators. When the validation rule changes — and it will — you have to find and update all four. You will miss one. That missed copy will live in a low-traffic code path, untested, silently accepting invalid emails for months. This is the concrete cost of DRY violations: not theoretical elegance, but very real bugs.

But DRY violations are more subtle than copy-paste. Two classes that independently track a "discount percentage" for an order are a DRY violation even if neither class shares code. If the business rule about how discounts are calculated lives in two places, both must be kept in sync. A schema migration that adds a new discount type requires changes in both. Forgetting either breaks the system — and the system won't tell you it's broken until a customer is charged the wrong amount.

DRY's solution is abstraction: find the single authoritative location for each piece of knowledge, and have everything else refer to it. Functions, constants, modules, configuration, schema migrations, generated types — all of these are DRY mechanisms. The specific tool is less important than the principle: one place, one truth.

Why Follow the DRY Principle?

The DRY principle provides several compelling benefits:

  1. Single point of change: When a business rule changes, you update exactly one place. Every caller picks up the change automatically — no search-and-replace, no missed copies
  2. Fewer bugs: Duplicated logic is the leading source of divergence bugs — two copies start identical and drift apart as only one is updated
  3. Easier refactoring: Code with no duplication can be restructured with confidence; there are no hidden copies of the old logic waiting to contradict the refactored version
  4. Smaller codebase: Less code means less to read, understand, test, and maintain — a codebase that is 30% smaller due to extracted abstractions is genuinely 30% cheaper to work with
  5. Self-documenting intent: Named abstractions (functions, constants, types) communicate intent; const TAX_RATE = 0.08 says more than 0.08 scattered across a hundred files
  6. Improved testability: Logic that exists in one place can be tested in one place — exhaustively, with confidence that passing tests mean the logic is correct everywhere it's used

Let's compare DRY with closely related principles:

PrincipleCore StatementScopePrimary MechanismFocus
DRYEvery piece of knowledge has one authoritative representationKnowledge, logic, intentAbstraction, extractionEliminating duplication
WETWrite Everything Twice (anti-pattern / DRY violation)Code, logicCopy-paste, inline repetitionWhat DRY prevents
OAOOOnce And Only Once — functionality exists in exactly one placeFunctionality, behaviorRefactoring, consolidationImplementation instances
SSOTSingle Source of Truth — one authoritative data sourceData, state, configurationCanonical data storesData consistency

Key Distinctions:

  • DRY vs. OAOO: DRY is about knowledge — any fact, rule, or intent. OAOO is about functionality — any piece of behavior or code. DRY is broader; it applies to documentation, configuration, and schema in addition to code. OAOO is more specifically code-focused. In practice, they are deeply compatible and most teams use them interchangeably.
  • DRY vs. SSOT: SSOT is specifically about data — having one canonical source from which all other representations are derived. DRY is about logic and knowledge. A database that is the SSOT for user records is an application of both: DRY (no duplicate user-logic) and SSOT (one canonical store). SSOT is the data-layer manifestation of DRY.
  • DRY vs. WET: WET is the anti-pattern DRY prevents. It describes code where logic is freely duplicated across the codebase. WET code is the natural state of a project with no DRY discipline — it emerges from time pressure, copy-paste convenience, and lack of shared abstractions.

DRY in Depth

Understanding DRY requires recognizing its three main application areas.

Knowledge Duplication Categories:

  1. Code duplication: The most visible form. The same logic — validation, calculation, transformation — appears in multiple functions, classes, or files. Changes require updating every copy.

  2. Data duplication: The same value — a tax rate, a base URL, a configuration threshold — is hardcoded in multiple places. When the value changes, you search for every occurrence.

  3. Representation duplication: The same concept is expressed twice in different forms without a single source generating both. A database schema column and a TypeScript type that must always match. An API contract documented in a README and also in code, with no shared source.

DRY Application Strategies:

  • Extract functions: The most basic DRY move. Any block of logic used in more than one place becomes a named, reusable function. The name documents intent; the single body ensures consistency.
  • Named constants: Magic numbers and literal strings scattered through code are a DRY violation. A constant named MAX_LOGIN_ATTEMPTS is a single authoritative definition — change it in one place and it changes everywhere.
  • Type/schema generation: In typed systems, generating types from a single schema (database schema → TypeScript types via codegen, JSON Schema → class definitions) ensures the two representations never diverge.
  • Configuration centralization: Environment-specific values — API endpoints, feature flags, timeouts — live in one configuration file or environment variable set, not hardcoded in the functions that use them.
  • Template and mixin patterns: For languages with inheritance or mixins, common behavior shared across types can be extracted into a base class or mixin, not duplicated in each subtype.

When is Duplication Actually Appropriate?

DRY is a principle, not an absolute rule. Three legitimate exceptions exist:

  1. The Rule of Three: A single duplication is often acceptable as a placeholder. When the same logic appears a third time, it's reliably a pattern worth extracting. Premature extraction for just two instances can create wrong abstractions that are harder to change than the duplication they replaced.

  2. Accidental similarity: Two blocks of code that look similar but represent different knowledge should not be merged. Merging them creates a hidden coupling — changing one business rule forces you to split the abstraction apart again. The test: "If this logic changes for reason A, does it necessarily change in the same way for reason B?" If not, they are different knowledge.

  3. Performance-critical inlining: In hot paths where function call overhead is measurable, duplicating a small piece of logic for performance is a conscious, documented tradeoff — not a violation of discipline.

Concept Diagram

Here is a visual representation of how DRY transforms duplicated knowledge into a single authoritative source:

Beginner-Friendly Example

Let's start with the clearest illustration of DRY: a user registration and profile update system where email validation, username validation, and password strength rules each need to be applied in multiple places. The WET version duplicates validation logic in every handler. The DRY version extracts each rule to a single authoritative location.

from __future__ import annotations
import re
from dataclasses import dataclass
from typing import Optional


# ════════════════════════════════════════════════════════════════
# ❌ WET VERSION — Duplicated validation logic in every function
# ════════════════════════════════════════════════════════════════

def register_user_wet(email: str, username: str, password: str) -> str:
    # Email validation — inline, will be copy-pasted elsewhere
    if not re.fullmatch(r"[^@]+@[^@]+\.[^@]+", email):
        return "Invalid email address"
    if len(email) > 255:
        return "Email address too long"
    # Username validation — inline
    if len(username) < 3 or len(username) > 30:
        return "Username must be 3–30 characters"
    if not re.fullmatch(r"[a-zA-Z0-9_]+", username):
        return "Username can only contain letters, numbers, and underscores"
    # Password validation — inline
    if len(password) < 8:
        return "Password must be at least 8 characters"
    if not any(c.isupper() for c in password):
        return "Password must contain at least one uppercase letter"
    return "OK"


def update_profile_wet(email: str, username: str) -> str:
    # ⚠️ Same email validation logic — copied from register_user_wet
    if not re.fullmatch(r"[^@]+@[^@]+\.[^@]+", email):
        return "Invalid email address"
    if len(email) > 255:
        return "Email address too long"
    # ⚠️ Same username validation logic — copied again
    if len(username) < 3 or len(username) > 30:
        return "Username must be 3–30 characters"
    if not re.fullmatch(r"[a-zA-Z0-9_]+", username):
        return "Username can only contain letters, numbers, and underscores"
    return "OK"


def reset_password_wet(password: str, confirm_password: str) -> str:
    # ⚠️ Same password validation — copied a third time
    if len(password) < 8:
        return "Password must be at least 8 characters"
    if not any(c.isupper() for c in password):
        return "Password must contain at least one uppercase letter"
    if password != confirm_password:
        return "Passwords do not match"
    return "OK"
# Problem: if the email regex changes, you update 2+ places. Miss one = silent bug.


# ════════════════════════════════════════════════════════════════
# ✅ DRY VERSION — Single authoritative source for each rule
# ════════════════════════════════════════════════════════════════

@dataclass(frozen=True)
class ValidationResult:
    valid: bool
    error: Optional[str] = None

    @classmethod
    def ok(cls) -> "ValidationResult":
        return cls(valid=True)

    @classmethod
    def fail(cls, error: str) -> "ValidationResult":
        return cls(valid=False, error=error)


# ── Single authoritative validation functions ──────────────────

def validate_email(email: str) -> ValidationResult:
    """Single source of truth for email validation rules."""
    if not re.fullmatch(r"[^@]+@[^@]+\.[^@]+", email):
        return ValidationResult.fail("Invalid email address")
    if len(email) > 255:
        return ValidationResult.fail("Email address too long")
    return ValidationResult.ok()


def validate_username(username: str) -> ValidationResult:
    """Single source of truth for username rules."""
    if len(username) < 3 or len(username) > 30:
        return ValidationResult.fail("Username must be 3–30 characters")
    if not re.fullmatch(r"[a-zA-Z0-9_]+", username):
        return ValidationResult.fail("Username can only contain letters, numbers, and underscores")
    return ValidationResult.ok()


def validate_password(password: str) -> ValidationResult:
    """Single source of truth for password strength rules."""
    if len(password) < 8:
        return ValidationResult.fail("Password must be at least 8 characters")
    if not any(c.isupper() for c in password):
        return ValidationResult.fail("Password must contain at least one uppercase letter")
    return ValidationResult.ok()


# ── Handlers that REFER to the single source — never re-implement ─

def register_user(email: str, username: str, password: str) -> str:
    for result in [validate_email(email), validate_username(username), validate_password(password)]:
        if not result.valid:
            return result.error
    return "OK"


def update_profile(email: str, username: str) -> str:
    for result in [validate_email(email), validate_username(username)]:
        if not result.valid:
            return result.error
    return "OK"


def reset_password(password: str, confirm_password: str) -> str:
    result = validate_password(password)
    if not result.valid:
        return result.error
    if password != confirm_password:
        return "Passwords do not match"
    return "OK"


# ── Client ─────────────────────────────────────────────────────
if __name__ == "__main__":
    print("=== DRY Validation System ===\n")

    print("Register (valid):", register_user("jane@example.com", "jane_doe", "SecurePass1"))
    print("Register (bad email):", register_user("not-an-email", "jane_doe", "SecurePass1"))
    print("Update profile:", update_profile("jane@example.com", "jane_doe"))
    print("Reset password:", reset_password("SecurePass1", "SecurePass1"))
    print("Reset mismatch:", reset_password("SecurePass1", "Different1"))
    # If password rules change, only validate_password() needs updating.

Production-Ready Example

Now let's build a pricing engine — a richer, production-grade application of the DRY principle. An e-commerce platform applies tax rates, discount rules, and price formatting consistently across order summaries, invoices, cart previews, and admin reports. The WET approach scatters these calculations and constants everywhere. The DRY approach puts each piece of pricing knowledge in one authoritative location — and all consumers derive their behavior from it.

🐍 Python

from __future__ import annotations
from dataclasses import dataclass, field
from decimal import Decimal, ROUND_HALF_UP
from typing import Optional
from enum import Enum


# ════════════════════════════════════════════════════════════════
# SINGLE SOURCE — Pricing knowledge lives here and only here
# ════════════════════════════════════════════════════════════════

class TaxRegion(Enum):
    US_STANDARD  = "us_standard"
    EU_VAT       = "eu_vat"
    UK_VAT       = "uk_vat"
    TAX_EXEMPT   = "tax_exempt"


# ── Single authoritative tax rates ─────────────────────────────
TAX_RATES: dict[TaxRegion, Decimal] = {
    TaxRegion.US_STANDARD: Decimal("0.0875"),   # 8.75%
    TaxRegion.EU_VAT:      Decimal("0.20"),      # 20%
    TaxRegion.UK_VAT:      Decimal("0.20"),      # 20%
    TaxRegion.TAX_EXEMPT:  Decimal("0.00"),
}

# ── Single authoritative discount tiers ────────────────────────
DISCOUNT_TIERS: list[tuple[Decimal, Decimal]] = [
    (Decimal("1000"), Decimal("0.15")),   # 15% off orders over $1000
    (Decimal("500"),  Decimal("0.10")),   # 10% off orders over $500
    (Decimal("200"),  Decimal("0.05")),   # 5%  off orders over $200
]

# ── Single authoritative rounding rule ─────────────────────────
def round_currency(amount: Decimal) -> Decimal:
    """All monetary rounding in the system uses this one function."""
    return amount.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)

# ── Single authoritative discount calculator ───────────────────
def calculate_discount(subtotal: Decimal) -> Decimal:
    """Single source of truth for volume discount logic."""
    for threshold, rate in DISCOUNT_TIERS:
        if subtotal >= threshold:
            return round_currency(subtotal * rate)
    return Decimal("0.00")

# ── Single authoritative tax calculator ────────────────────────
def calculate_tax(amount_after_discount: Decimal, region: TaxRegion) -> Decimal:
    """Single source of truth for tax calculation."""
    rate = TAX_RATES[region]
    return round_currency(amount_after_discount * rate)

# ── Single authoritative price formatter ───────────────────────
def format_price(amount: Decimal) -> str:
    """All price display in the system uses this one formatter."""
    return f"${amount:,.2f}"


# ════════════════════════════════════════════════════════════════
# CONSUMERS — All derive from the single source above
# ════════════════════════════════════════════════════════════════

@dataclass
class LineItem:
    name:      str
    unit_price: Decimal
    quantity:  int

    @property
    def subtotal(self) -> Decimal:
        return round_currency(self.unit_price * self.quantity)


@dataclass
class PricingBreakdown:
    subtotal:  Decimal
    discount:  Decimal
    tax:       Decimal
    total:     Decimal
    region:    TaxRegion

    def display(self, title: str = "Pricing Summary") -> None:
        print(f"\n  ── {title} ──")
        print(f"  Subtotal:    {format_price(self.subtotal)}")  # Uses single formatter
        if self.discount > 0:
            print(f"  Discount:   -{format_price(self.discount)}")
        print(f"  Tax ({self.region.value}): {format_price(self.tax)}")
        print(f"  ─────────────────────")
        print(f"  Total:       {format_price(self.total)}")


def price_order(items: list[LineItem], region: TaxRegion) -> PricingBreakdown:
    """Uses every single-source function — no inline calculation."""
    subtotal  = round_currency(sum(item.subtotal for item in items))
    discount  = calculate_discount(subtotal)          # Single source
    after_discount = subtotal - discount
    tax       = calculate_tax(after_discount, region) # Single source
    total     = after_discount + tax
    return PricingBreakdown(subtotal, discount, tax, total, region)


# ── Multiple consumers — all using the same single sources ──────

def generate_cart_preview(items: list[LineItem], region: TaxRegion) -> str:
    breakdown = price_order(items, region)
    # Uses format_price — the single formatter — no inline formatting
    return f"Cart: {format_price(breakdown.subtotal)} → Total: {format_price(breakdown.total)}"


def generate_invoice_line(items: list[LineItem], region: TaxRegion) -> str:
    breakdown = price_order(items, region)
    tax_rate  = TAX_RATES[region] * 100    # Single source for rate display
    return (
        f"Invoice | Sub: {format_price(breakdown.subtotal)} | "
        f"Disc: {format_price(breakdown.discount)} | "
        f"Tax({tax_rate:.1f}%): {format_price(breakdown.tax)} | "
        f"Total: {format_price(breakdown.total)}"
    )


# ── Client ─────────────────────────────────────────────────────
if __name__ == "__main__":
    items = [
        LineItem("Laptop",   Decimal("899.99"), 1),
        LineItem("Keyboard", Decimal("149.99"), 2),
        LineItem("Monitor",  Decimal("399.00"), 1),
    ]

    print("=== DRY Pricing Engine ===")
    for region in [TaxRegion.US_STANDARD, TaxRegion.EU_VAT, TaxRegion.TAX_EXEMPT]:
        breakdown = price_order(items, region)
        breakdown.display(f"Order — {region.value}")

    print("\n" + generate_cart_preview(items, TaxRegion.US_STANDARD))
    print(generate_invoice_line(items, TaxRegion.EU_VAT))
    # If the US tax rate changes, update TAX_RATES once.
    # Cart preview, invoice, order summary, and admin report all update automatically.

Real-World Use Cases

The DRY principle applies far beyond validation and pricing. Here are the most impactful areas where DRY prevents the most bugs.

Configuration and constants: Magic numbers and strings embedded across a codebase are a DRY violation. MAX_RETRY_ATTEMPTS = 3 defined once, referenced everywhere, is DRY. The same 3 copy-pasted into five retry loops, with one loop that was later updated to 5 while the others weren't, is a real production bug with a real outage.

API contracts and types: In full-stack applications, the API request/response shape is duplicated when the backend defines it in Python and the frontend defines it again in TypeScript. Code generation from a single schema (OpenAPI, GraphQL, Protobuf) eliminates this. When the schema changes, both backend and frontend types regenerate from the same source — they cannot diverge.

Database queries: An application that constructs the same SQL fragment — WHERE deleted_at IS NULL AND active = 1 — inline in a dozen query functions has duplicated business knowledge about what "active" means. A single active_users query builder or named scope is the DRY solution. When the definition of "active" changes (adding a suspended flag, for instance), one change propagates everywhere.

Business rules in multiple tiers: Discount eligibility rules that exist in the backend (for processing) and also in the frontend (for display) and also in the database (as a CHECK constraint) are triplicated. A single canonical rule — even if it must be compiled to different environments — reduces the three places that must be updated on every rule change to one authoritative source.

Error messages: User-facing error strings hardcoded in multiple places will inevitably say different things for the same error in different parts of the application. A centralized message registry ensures every consumer says exactly the same thing for the same condition.


Language-Specific Mistakes and Anti-Patterns

❌ Mistake #1: Magic Numbers Scattered Across Functions

# BAD: 8 appears in three separate places — if the minimum changes, which do you update?
def register_user(password: str) -> bool:
    return len(password) >= 8    # ← knowledge

def validate_api_key(key: str) -> bool:
    return len(key) >= 8         # ← different knowledge that looks identical

def reset_password(new_password: str) -> bool:
    if len(new_password) < 8:   # ← same password knowledge, duplicated
        raise ValueError("Too short")
    return True

# GOOD: Named constant — one authoritative definition
MIN_PASSWORD_LENGTH = 8   # Change here → changes everywhere

def validate_password_length(password: str) -> bool:
    return len(password) >= MIN_PASSWORD_LENGTH

# register_user and reset_password both call validate_password_length

❌ Mistake #2: Duplicated Formatting Logic

# BAD: Currency formatting repeated everywhere
def show_cart(total: float) -> str:
    return f"Total: ${total:.2f}"    # ← formatting knowledge

def print_receipt(amount: float) -> str:
    return f"Charged: ${amount:.2f}" # ← same formatting, duplicated

def send_invoice(price: float) -> str:
    return f"Amount: ${price:.2f}"   # ← third copy

# GOOD: Single formatter
def format_currency(amount: float) -> str:
    """Single source for all monetary display formatting."""
    return f"${amount:,.2f}"

def show_cart(total: float)      -> str: return f"Total: {format_currency(total)}"
def print_receipt(amount: float) -> str: return f"Charged: {format_currency(amount)}"
def send_invoice(price: float)   -> str: return f"Amount: {format_currency(price)}"

Frequently Asked Questions

Q1: Is DRY just about avoiding copy-pasted code?

No — DRY is about knowledge, not just text. Two functions can have entirely different code but still violate DRY if they both independently implement the same business rule. Conversely, two functions that look similar but represent different rules should not be merged — that would create accidental coupling. The question to ask is: "If this rule changes, should both places change in the same way?" If yes, they're the same knowledge and should be unified. If no, they're different knowledge that happens to look similar.

Q2: Can DRY be over-applied?

Yes. The most common over-application is creating a "universal" abstraction from two pieces of code that happen to look similar but represent different things. When requirement A changes, you're forced to break the shared abstraction apart — leaving you worse off than if you'd never merged them. This is why the Rule of Three is a practical guide: one occurrence is fine, two is worth noting, three is reliably a pattern worth extracting.

Q3: What is the difference between DRY and abstraction?

DRY is the principle — eliminate duplicated knowledge. Abstraction is the mechanism — functions, classes, modules, constants, generated types. DRY tells you when to abstract (when the same knowledge appears in more than one place). Abstraction gives you the tools to do it. DRY without abstraction is just an intention; abstraction without DRY gives you abstractions that don't correspond to real duplication.

Q4: How does DRY relate to the Single Responsibility Principle (SRP)?

They are complementary and mutually reinforcing. SRP says each module should have exactly one reason to change. DRY says each piece of knowledge should live in exactly one place. A DRY violation (the same rule in two places) usually means one of them will change while the other doesn't — which is also an SRP violation. Applying DRY by extracting knowledge into a single named location naturally produces SRP-compliant classes and functions.

Q5: Does DRY apply to tests?

Partially. Test code is different from production code — readable, independent tests are valuable even if they share setup code. Excessive DRY in tests produces helper functions that obscure what each test is actually testing. The convention "Arrange, Act, Assert" in each test method, even at the cost of some duplication, is intentional. However, duplicated test logic (the assertion patterns, shared fixtures, setup that is clearly identical) should still be extracted into helpers. The threshold for extraction in tests is higher than in production code.

Q6: How do I identify DRY violations in a codebase I didn't write?

Search for: (1) the same regex or validation logic in multiple files, (2) the same magic number (0.08, 3, 50) appearing more than twice without a named constant, (3) the same error message string in multiple handlers, (4) parallel data structures that must be updated together (a TypeScript interface and a Python Pydantic model with the same fields), (5) Ctrl+F your way to copies — if you search for a business term and it appears in more than one implementation, that's a candidate.


Key Takeaways

🎯 Core Concept: DRY states that every piece of knowledge in a system — logic, rules, data values, intent — must have a single, unambiguous, authoritative representation. Every other part of the system refers to that representation rather than re-implementing it. Changes propagate automatically from the single source to every consumer, making it impossible to update one copy while leaving another stale.

🔑 Key Benefits:

  • Single point of change: Update one place, propagate everywhere — no search-and-replace risk
  • Divergence prevention: Two copies of the same rule will inevitably drift; one copy cannot
  • Smaller surface area: Less code to read, test, and maintain
  • Named intent: Constants and extracted functions name the why, not just the what
  • Compound confidence: A test that passes for the single authoritative implementation passes for all consumers

🌐 Language-Specific Highlights:

  • Python: Use module-level constants for magic numbers; @dataclass and @property to avoid duplicated computed values; ABC + @abstractmethod to enforce shared interfaces; type aliases to avoid repeated complex type annotations
  • TypeScript: Centralize types in a types/ folder and import them everywhere; as const enums for string constants; avoid parallel interface definitions on frontend and backend — use codegen from a shared schema
  • Java: Define constants in a dedicated BusinessRules or AppConstants class; use record for immutable value objects shared across the codebase; extract repeated Optional/null-check patterns into utility methods
  • JavaScript: Define API base URLs in a single apiClient module; use module-level const for configuration values; export shared utilities from a utils/ index file
  • C#: Use internal sealed classes for implementation, public interface for the shared contract; record types for immutable shared value objects; static extension methods for shared transformations
  • PHP: Use readonly class properties for value objects; centralize route constants and error messages in dedicated classes; use constructor property promotion to reduce boilerplate
  • Go: Define sentinel errors as package-level var — they become the single source for error identity checks; use const blocks for related constants; export only what's needed to keep consumers from depending on internal detail
  • Rust: Implement Display and From traits once instead of writing conversion logic in every callsite; use const for magic values; OnceLock for lazily-compiled regexes — one definition, zero-cost reuse
  • Dart: Use const constructors for design tokens and configuration objects; extension methods to add shared behavior to existing types without subclassing; typedef for shared function signatures
  • Swift: Define design tokens in a enum AppSpacing or struct Theme; use extension to add shared computed properties to built-in types; protocol + extension for default implementations shared across conforming types
  • Kotlin: Use object for singleton constants and utility namespaces; companion object for factory methods and constants tied to a type; extension functions for shared transformations; data class for value objects used in multiple layers

📋 When DRY Clearly Applies:

  • The same validation rule, calculation, or transformation appears in two or more places
  • A magic number or string literal appears more than twice without a name
  • Two data structures must always be updated together (they represent the same schema)
  • An error message or user-facing string is hardcoded in multiple handlers
  • The same null-check, error-handling, or logging pattern is copy-pasted across multiple functions

⚠️ When to Hold Off:

  • You have only two occurrences and the "right" abstraction isn't obvious — wait for the third (Rule of Three)
  • Two pieces of code look similar but change for different reasons — merging them creates fragile coupling
  • Test code where duplication improves readability and independence of each test case
  • Performance-critical paths where the abstraction adds measurable overhead

🛠️ Best Practices Across Languages:

  1. Name every magic value: A constant with a good name communicates intent and creates a single update point
  2. Extract the pattern, not the code: Before extracting a function, ask if the two callers share the same business concept — if not, the similarity is accidental
  3. Use code generation for cross-layer duplication: API types, database schema types, and configuration structures should be generated from a single source, not hand-maintained in parallel
  4. Centralize configuration: All environment-specific values — URLs, timeouts, limits — live in one configuration module that is imported, never inlined
  5. Let the Rule of Three guide extraction timing: Two occurrences is a note; three is a commitment
  6. Distinguish essential from accidental similarity: Two functions that do the same thing for the same reason should be unified; two that look the same but serve different domains should remain separate
  7. Document the single source: When a constant, function, or module is the authoritative source for a piece of knowledge, say so in the docstring — future developers need to know where to find it

💡 Common Pitfalls:

  • Merging accidentally similar logic: Two loops that iterate over different domains in the same way are not the same knowledge — merging them produces a fragile "general" function that breaks when either domain evolves
  • Hardcoding in tests: Test data that repeats magic numbers from production code creates a hidden coupling — if the production constant changes, tests may still pass while testing the wrong thing
  • Over-centralizing configuration: A single global config object that grows to hold every value in the system becomes a dependency magnet — group configuration by domain
  • DRY at the wrong layer: Sharing database queries between two services that happen to need the same data can couple microservices — sometimes the duplication is intentional isolation
  • Ignoring generated duplication: Checked-in generated files (ORM models, API clients) that diverge from their source schema are a DRY violation hiding in plain sight — regenerate on every schema change

The DRY principle is the foundation of maintainable software. Every duplicated piece of knowledge is a future bug waiting for a change request to trigger it. Applying DRY is not about aesthetic tidiness — it is about building a system where updating a business rule means updating one line, not hunting down every place that rule was re-implemented.


Want to explore related principles? Check out our guides on: