Anti-Pattern · WET

Write Everything Twice

The anti-pattern DRY exists to prevent — duplicated knowledge and logic.

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

WET — most commonly expanded as Write Everything Twice (or Write Every Time, Waste Everyone's Time) — is the anti-pattern that the DRY principle exists to prevent. WET code is what you get when the same knowledge — logic, validation rules, data values, intent — is implemented independently in multiple places. It is not a prescriptive principle to follow; it is a diagnostic label for a category of technical debt. Recognizing WET patterns is the first step to applying DRY effectively, and understanding why WET is harmful — not just that it is — makes DRY discipline sustainable rather than dogmatic.

In this comprehensive guide, we'll explore WET patterns, their concrete costs, and how to eliminate them with real-world examples in Python, TypeScript, Java, JavaScript, C#, PHP, Go, Rust, Dart, Swift, and Kotlin — complete with language-specific pitfalls and the refactoring techniques that resolve them.

Table of Contents

What is WET Code?

WET code is code where the same knowledge is expressed in more than one place. The two (or three, or ten) expressions start as identical copies. They diverge over time as requirements evolve and each copy is updated independently — with different people updating different copies at different times, with no mechanism to ensure consistency.

The most visible form of WET is copy-paste: a developer writes a function to validate an email, then copies it into a second file to avoid creating a shared module. Weeks later, the validation rule is tightened — one copy gets updated and the other doesn't. Emails that the system should reject now pass through one code path because the old validation logic is still running there.

But WET is broader than copy-paste. WET patterns include:

Inline magic literals: if (retryCount < 3) in five different functions. Each 3 is a copy of the same business decision. When the decision changes to 5, four of the five functions are updated and one is not — producing inconsistent retry behavior that is nearly impossible to debug.

Parallel data structures: A database schema has a status column with values "pending", "approved", "rejected". The TypeScript frontend has a Status enum with the same values. The Python backend has a StatusEnum. All three must stay in sync. Any discrepancy — a new status added to the schema but not to the TypeScript enum — produces a silent bug or a runtime error.

Duplicated business rules across layers: Discount eligibility logic in the frontend (for display), in the backend API (for enforcement), and in the database (as a constraint). All three encode the same business rule independently. When the rule changes, it must change in three places — and the probability of a missed update is close to 1 over a long enough timeline.

Documentation that duplicates code: Comments that re-state what code does, not why, will lie as soon as the code changes without updating the comment. This is a WET violation: the knowledge is expressed twice, in code and in comment, and the two will diverge.

WET code is the natural state of a codebase under time pressure with no shared-abstraction discipline. It accretes gradually — each individual copy feels like the faster choice at the time of writing. Its cost is paid not at write time but at change time, with interest.

Why WET Code Is Harmful

WET code imposes costs that compound over time:

  1. Divergence bugs: The core harm of WET. Two copies start identical. One gets updated. The other doesn't. Now the system has two codepaths enforcing two different rules for the same business concept. These bugs are particularly insidious because the code is correct in the updated copy — only an audit of all copies reveals the discrepancy.
  2. Search-and-replace maintenance: Any change to a WET piece of knowledge requires finding every copy. Search-and-replace in a large codebase is error-prone. Copies may live in files you don't know about, in test utilities, in migration scripts, in documentation generators.
  3. Test multiplication: Each copy of a piece of logic needs to be tested independently. A single extraction would require one test suite. Five copies require five — and if the logic is wrong in all five, you write five test suites that all pass for the wrong reason.
  4. Cognitive load: A developer reading the codebase encounters the same logic in multiple places and cannot determine which is authoritative. Is one the "real" version? Are they intentionally different? This uncertainty slows reading and makes refactoring risky.
  5. Onboarding friction: New developers learn the codebase by reading it. A WET codebase teaches contradictions — two implementations of the "same" rule that produce different results in edge cases. The new developer doesn't know which to trust, so they copy the nearest one, perpetuating the WET pattern.
ConceptDescriptionRelationship to WET
WETDuplicated knowledge — same logic, values, or rules in multiple placesThe anti-pattern itself
DRYSingle authoritative representation for each piece of knowledgeThe principle WET violates
OAOOFunctionality exists in exactly one placeA more code-focused formulation of the same ideal
SSOTOne canonical data source, all others derivedData-layer WET violation when multiple stores hold the same facts
Technical DebtFuture cost incurred for current shortcutsWET is a specific, high-interest form of technical debt

Key Distinction — WET vs. Acceptable Duplication: Not all similar-looking code is WET. Two functions that share surface-level structure but represent different business knowledge should not be merged. Merging them creates a dependency between two rules that should evolve independently. The litmus test: "If this rule changes, does the other place change for the same reason?" If no, the similarity is accidental — not a WET violation.

WET Patterns in Depth

Understanding WET requires being able to identify its specific forms in production code.

WET Pattern Categories:

  1. Copy-paste duplication: The same block of code appears in multiple functions or files. The simplest and most visible form. Identifiable by Ctrl+F on any distinctive line.

  2. Magic literal duplication: The same constant value — 0.08, "pending", 3, "api.example.com" — appears as a literal in multiple places. The value is the knowledge; every literal is a copy.

  3. Structural duplication: The same concept is defined as multiple parallel data structures that must be kept in sync — a database schema column and a TypeScript type, an API response shape and a frontend model.

  4. Representational duplication: The same information exists in two forms that must be manually synchronized — a README that documents API parameters that also exist in code, a comment that re-states what a function does.

  5. Cross-layer rule duplication: The same business rule is enforced independently in multiple system layers — frontend validation, backend validation, and a database constraint all encode the same constraint.

WET's Lifecycle:

WET code follows a predictable lifecycle. It starts as a copy: someone copies a function to get something working fast. The copy is identical to the original. No bug. The first change request arrives. A developer updates the original and doesn't know about the copy — or does know and decides to "do it later." The copy is now stale. Six months later, a bug report arrives. The debugging session reveals two copies with different behavior. The fix is applied to both — but the root cause (two copies) is not addressed. The cycle repeats.

Anatomy of a WET System

Here is a structural illustration of how WET proliferates through a system and what the DRY alternative looks like:

Beginner-Friendly Example

Let's walk through a concrete WET scenario and its DRY refactoring: a notification system where push, email, and SMS notifications each need to format the same message, apply the same character limits, and log the same delivery metadata — but each was implemented independently.

from __future__ import annotations
import re
from datetime import datetime
from typing import Optional


# ════════════════════════════════════════════════════════════════
# WET VERSION — Each channel independently implements shared logic
# ════════════════════════════════════════════════════════════════

class WetEmailNotifier:
    MAX_SUBJECT_LENGTH = 78
    MAX_BODY_LENGTH    = 10_000  # ← knowledge, defined locally

    def send(self, recipient: str, subject: str, body: str) -> dict:
        # Email validation — will be copy-pasted to SMS and Push
        if not re.fullmatch(r"[^@]+@[^@]+\.[^@]+", recipient):
            return {"success": False, "error": "Invalid recipient"}
        # Truncation — will be copy-pasted
        if len(subject) > self.MAX_SUBJECT_LENGTH:
            subject = subject[:self.MAX_SUBJECT_LENGTH - 3] + "..."
        if len(body) > self.MAX_BODY_LENGTH:
            body = body[:self.MAX_BODY_LENGTH - 3] + "..."
        # Delivery logging — will be copy-pasted
        timestamp = datetime.utcnow().isoformat()
        print(f"LOG: [{timestamp}] EMAIL → {recipient}: {subject[:30]}")
        return {"success": True, "channel": "email", "logged_at": timestamp}


class WetSmsNotifier:
    MAX_LENGTH      = 160
    MAX_BODY_LENGTH = 10_000    # ← same constant as email — a WET copy

    def send(self, phone: str, message: str) -> dict:
        if not re.fullmatch(r"\+?\d{10,15}", phone):
            return {"success": False, "error": "Invalid phone number"}
        if len(message) > self.MAX_LENGTH:
            message = message[:self.MAX_LENGTH - 3] + "..."
        # Delivery logging — exact copy of email's logging code
        timestamp = datetime.utcnow().isoformat()
        print(f"LOG: [{timestamp}] SMS → {phone}: {message[:30]}")
        return {"success": True, "channel": "sms", "logged_at": timestamp}


class WetPushNotifier:
    MAX_TITLE_LENGTH = 50
    MAX_BODY_LENGTH  = 10_000   # ← third copy of the same constant

    def send(self, device_token: str, title: str, body: str) -> dict:
        if len(device_token) < 32:
            return {"success": False, "error": "Invalid device token"}
        if len(title) > self.MAX_TITLE_LENGTH:
            title = title[:self.MAX_TITLE_LENGTH - 3] + "..."
        # Delivery logging — third copy, slightly different format
        timestamp = datetime.utcnow().isoformat()
        print(f"LOG: [{timestamp}] PUSH → {device_token[:8]}…: {title}")
        return {"success": True, "channel": "push", "logged_at": timestamp}

# If the log format changes: 3 places to update. Miss one = divergence.
# If MAX_BODY_LENGTH changes: 3 places to update. Miss one = divergence.


# ════════════════════════════════════════════════════════════════
# DRY VERSION — Shared knowledge extracted to single sources
# ════════════════════════════════════════════════════════════════

MAX_BODY_LENGTH   = 10_000   # One definition — all channels refer to this
EMAIL_SUBJECT_MAX = 78
SMS_MESSAGE_MAX   = 160
PUSH_TITLE_MAX    = 50

def truncate(text: str, max_length: int, suffix: str = "...") -> str:
    """Single source for all text truncation."""
    if len(text) <= max_length:
        return text
    return text[:max_length - len(suffix)] + suffix

def log_delivery(channel: str, recipient: str, preview: str) -> str:
    """Single source for all delivery logging format."""
    timestamp = datetime.utcnow().isoformat()
    print(f"LOG: [{timestamp}] {channel.upper()}{recipient}: {preview[:30]}")
    return timestamp

def validate_email_address(email: str) -> Optional[str]:
    if not re.fullmatch(r"[^@]+@[^@]+\.[^@]+", email):
        return "Invalid recipient email"
    return None

def validate_phone_number(phone: str) -> Optional[str]:
    if not re.fullmatch(r"\+?\d{10,15}", phone):
        return "Invalid phone number"
    return None

def validate_device_token(token: str) -> Optional[str]:
    if len(token) < 32:
        return "Invalid device token"
    return None


class EmailNotifier:
    def send(self, recipient: str, subject: str, body: str) -> dict:
        if error := validate_email_address(recipient):
            return {"success": False, "error": error}
        subject = truncate(subject, EMAIL_SUBJECT_MAX)
        body    = truncate(body, MAX_BODY_LENGTH)
        ts = log_delivery("email", recipient, subject)
        return {"success": True, "channel": "email", "logged_at": ts}


class SmsNotifier:
    def send(self, phone: str, message: str) -> dict:
        if error := validate_phone_number(phone):
            return {"success": False, "error": error}
        message = truncate(message, SMS_MESSAGE_MAX)
        ts = log_delivery("sms", phone, message)
        return {"success": True, "channel": "sms", "logged_at": ts}


class PushNotifier:
    def send(self, device_token: str, title: str, body: str) -> dict:
        if error := validate_device_token(device_token):
            return {"success": False, "error": error}
        title = truncate(title, PUSH_TITLE_MAX)
        body  = truncate(body, MAX_BODY_LENGTH)
        ts = log_delivery("push", device_token[:8] + "…", title)
        return {"success": True, "channel": "push", "logged_at": ts}


if __name__ == "__main__":
    print("=== DRY Notification System ===\n")
    email = EmailNotifier()
    sms   = SmsNotifier()
    push  = PushNotifier()
    print(email.send("jane@example.com", "Your order has shipped!", "Hello Jane..."))
    print(sms.send("+12025551234", "Your order shipped! Track at example.com/track"))
    print(push.send("a" * 64, "Order Shipped", "Your item is on the way"))
    print(email.send("not-an-email", "Subject", "Body"))

Production-Ready Example

The WET pattern's most damaging form in production is cross-service business rule duplication — the same eligibility criteria, discount thresholds, or access control rules independently maintained in multiple services. When one service's version of the rule diverges from another's, data inconsistencies accumulate silently until a customer notices.

The pattern of extracting shared business rules into a single shared library, rules module, or policy object applies identically across all 11 languages — the language-specific implementations follow the same structure as the beginner example above, scaled up to richer domain objects. The architectural insight is the same regardless of language: any rule that must be true in multiple places belongs in exactly one place, and all other places reference it.

A shared PricingRules module, AccessPolicy class, or OrderEligibility validator used by every layer (frontend, backend, worker, admin) is the production DRY resolution. Adding a new business rule to that module automatically propagates to all consumers — no search-and-replace, no missed services, no silent divergence.


Real-World WET Scenarios

WET patterns appear in every layer of production systems. Here are the highest-impact occurrences and their DRY resolutions.

Authentication and authorization: Checking if user.role == "admin" inline in every API endpoint is WET. When a new "superadmin" role is introduced, every endpoint must be updated. A single hasAdminAccess(user) function — add the new role in one place.

Database query conditions: WHERE deleted_at IS NULL AND active = TRUE embedded in a dozen queries is WET. When the definition of "active" changes (adding a suspended flag), one activeScope() or named query builder propagates the change everywhere.

Error handling boilerplate: The same try/catch → log → return error response pattern copy-pasted into every service method. An extracted safeExecute(context, block) wrapper changes the logging format or error shape in one place.

Cross-stack type duplication: A backend Pydantic model and a frontend TypeScript interface for the same API response are WET. Code generation from a shared schema (OpenAPI, Protobuf, GraphQL SDL) eliminates the divergence entirely.

Localization strings: Error messages hardcoded in multiple views will inevitably say different things for the same error. A centralized messages.ts or strings.xml ensures every view says exactly the same thing for the same condition.


Language-Specific WET Patterns

The most common Python WET: the same re.compile() pattern written inline in multiple functions; identical dataclass field sets copy-pasted across modules; the same try/except/log pattern repeated in every async handler. Resolution: compile regexes at module level, define shared types in a types.py, extract exception handling into a decorator.

Frequently Asked Questions

Q1: Is all duplicated code WET?

No. The crucial distinction is between essential and accidental similarity. Two functions that look identical but represent different business concepts that evolve independently should not be merged — merging creates a coupling that forces both to change together when they shouldn't. The test: "If one of these changes, does the other change for the same reason?" If no, the similarity is accidental, not WET.

Q2: What is the Rule of Three and how does it apply?

The Rule of Three says: tolerate one duplication, note a second, extract on the third. A single duplication might be deliberate isolation. Two might be coincidence. Three is reliably a pattern worth extracting. The rule prevents over-extracting abstractions from two similar cases where the right abstraction isn't clear yet — wrong abstractions are harder to fix than the duplication they replaced.

Q3: How do I identify WET in a codebase I inherited?

Search for: the same literal values (0.08, "pending", "/api/v1") in multiple files; the same method name in multiple classes without a shared interface; functions named validateEmailInService, validateEmailInController, validateEmailInAdmin; and comments like // same as above or // copied from X. Static analysis tools (jscpd, PMD CPD, SonarQube) can detect structural clones automatically.

Q4: Can WET be in documentation, not just code?

Yes, and it is particularly expensive there. A README that documents an API parameter's format, with the same format also expressed as a validation regex in code, is a WET violation. When the format changes, the README may not be updated. The DRY resolution is a docstring generated from the code's single source, or a documentation generation tool that pulls descriptions from the code itself.

Q5: What is the relationship between WET and the Open/Closed Principle?

WET code tends to violate OCP: when a business rule is duplicated across multiple classes, adding a new variant requires editing every class that contains the rule. An OCP-compliant design puts the rule in one place. WET is the precondition for this OCP violation — the more copies of a rule, the more edit points every extension requires.


Key Takeaways

🎯 Core Concept: WET is the anti-pattern that DRY exists to prevent. It describes code where the same knowledge — logic, rules, data values, intent — exists in more than one place. WET code starts as identical copies and diverges over time as only some copies are updated when requirements change. The divergence produces bugs that are difficult to find because each copy appears locally correct.

🔑 Why WET Causes Real Damage:

  • Divergence bugs: Two copies with one updated and one stale, silently producing different behavior for the same business concept
  • Search-and-replace maintenance: Every change requires finding every copy — and missing one creates a bug
  • Test multiplication: Each copy needs its own tests; bugs in duplicated logic require fixes in multiple test suites
  • Cognitive confusion: Developers cannot determine which copy is authoritative, slowing reading and refactoring
  • Onboarding debt: New developers learn from WET code and perpetuate the pattern

🌐 Language-Specific WET Hotspots:

  • Python: Inline regex literals, copy-pasted dataclass fields, repeated try/except/log patterns
  • TypeScript: Parallel interface definitions between frontend and backend, raw string status literals
  • Java: Same constant value in multiple classes, copy-pasted LINQ/stream filter conditions
  • JavaScript: Copy-pasted fetch() headers and base URLs, repeated DOM manipulation patterns
  • C#: Duplicated LINQ query conditions, parallel DTO and domain model field definitions
  • PHP: Copy-pasted Blade/Twig template fragments, repeated validation in controllers
  • Go: Same struct definition in handler and domain layers, literal error string duplication
  • Rust: Same match arms across multiple functions, duplicate enum-to-string conversions
  • Dart: Hardcoded EdgeInsets/Colors values in widgets, duplicated route strings
  • Swift: Copy-pasted URLRequest construction, raw UserDefaults key strings
  • Kotlin: Same when expression in multiple locations, repeated runCatching boilerplate

📋 The Refactoring Process for WET Code:

  1. Identify the duplicated knowledge — what business concept is expressed in multiple places?
  2. Determine the authoritative home — which location is the right place for this knowledge?
  3. Extract to that single location with a clear, intent-communicating name
  4. Replace all copies with references to the single source
  5. Test the single source exhaustively — all former copies are now covered by one test suite

⚠️ When Duplication Is Acceptable:

  • Two similar implementations serving different domains that should evolve independently (accidental similarity)
  • A single duplication where the right abstraction isn't clear yet — wait for a third occurrence
  • Test code where independence of test cases justifies some setup duplication
  • Performance-critical paths where the abstraction overhead is measurable and documented

WET is not a character flaw — it is the natural result of building under time pressure without shared-abstraction discipline. Recognizing it, naming it, and refactoring toward DRY is a continuous practice. The goal is not a perfectly WET-free codebase but one where every new piece of shared knowledge finds its single home immediately.


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