PushBackLog

Don't Repeat Yourself (DRY)

Soft enforcement Complete by PushBackLog team
Topic: quality Topic: supportability Methodology: DRY Skillset: any Technology: generic Stage: execution Stage: review

Don’t Repeat Yourself (DRY)

Status: Complete
Category: Clean Code
Default enforcement: Soft
Author: PushBackLog team


Tags

  • Topic: quality, supportability
  • Methodology: DRY
  • Skillset: any
  • Technology: generic
  • Stage: execution, review

Summary

Every piece of knowledge should have a single, authoritative representation in a system. When the same logic, data, or structure appears in more than one place, a change to it requires finding and updating every instance — a process that is error-prone and often incomplete.


Rationale

Knowledge, not just code

DRY is commonly misread as “don’t write the same code twice”. The actual principle from Hunt and Thomas (The Pragmatic Programmer, 1999) is more precise: “Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.” Knowledge includes logic, business rules, schema definitions, configuration, and documentation — not just syntactically similar code blocks.

Two for-loops that happen to look alike but iterate over unrelated concepts are not a DRY violation. Merging them would create false coupling. Two places in the codebase that independently enforce the same business rule (e.g., “order discounts cap at 20%”) are a DRY violation — even if the code looks different — because they represent the same piece of knowledge that must be kept in sync.

The maintenance cost of duplication

Duplication is a deferred cost that compounds. The first time a rule changes (“discounts cap at 25% now”), a developer finds and updates one location. They may not know there are three others. The bug that results is subtle — the system behaves inconsistently depending on which code path was triggered. This class of bug is extremely common and disproportionately hard to trace.

DRY and abstraction

Removing duplication requires introducing an abstraction: a function, a module, a constant, a utility class. This is its own form of complexity. The key question is whether the abstraction accurately models a real concept or is merely structural coincidence. Premature DRY — abstracting two things that happen to look the same but represent different knowledge — creates coupling between things that should evolve independently. DRY applied correctly reduces incidental coupling; DRY applied incorrectly creates accidental coupling.


Guidance

When to extract (and when not to)

SituationRecommendation
Same business rule in multiple placesExtract immediately — this is true duplication
Same code structure, different conceptLeave separate — don’t couple unrelated things
Two callsites, rule just arrivedWait for a third case (rule of three) before abstracting
Database schema and API response mirror each otherSingle source of truth (schema, then generate types)
Documentation duplicates code commentsChoose one — inline comments or external docs

Common extraction strategies

  • Constants and enums for repeated literals and configuration values
  • Helper functions for repeated algorithm logic
  • Shared module/package for business rules used across services
  • Code generation from a single schema (API types, DB types, form validation from one schema)
  • Single source of truth for configuration — one config file, not one per environment with overlapping keys

The WET trap

“WET” (Write Everything Twice, or We Enjoy Typing) describes codebases where duplication has been left in place. The telltale sign is a bug fix that requires the same change to be made in multiple files. If your team finds itself saying “remember to also update X when you update Y”, there is a DRY violation.


Examples

Duplicated business rule

// In OrderService
if (discount > 0.25) {
  throw new Error('Discount cannot exceed 25%');
}

// In CartService (different team, same rule)
if (couponValue / total > 0.25) {
  throw new Error('Discount cannot exceed 25%');
}

// In AdminPanel (frontend validation)
if (discountField > 25) {
  showError('Max discount is 25%');
}

When the rule changes to 30%, three places must be found and updated consistently.

DRY version

// pricing-rules.ts — single authoritative source
export const MAX_DISCOUNT_RATE = 0.25;

export function validateDiscount(rate: number): void {
  if (rate > MAX_DISCOUNT_RATE) {
    throw new ValidationError(`Discount cannot exceed ${MAX_DISCOUNT_RATE * 100}%`);
  }
}

All three callsites import and call validateDiscount. The rule exists once.

False DRY (don’t extract this)

// User registration
const today = new Date().toISOString().split('T')[0];

// Order processing
const today = new Date().toISOString().split('T')[0];

Same code structure, but these two today variables serve unrelated purposes and may legitimately diverge (one might become “registration timestamp”, one might become “order date in a specific timezone”). Abstracting them couples two independent concepts.


Anti-patterns

1. Copy-paste programming

Copying code to solve a similar problem, rather than extracting a shared abstraction. Each copy starts identical but drifts over time as only some are updated.

2. Duplicated validation across layers

Validating the same constraint in the UI, the API controller, the service layer, and the database trigger separately. Each layer guards itself defensively, but rule changes require co-ordinated updates across all layers.

3. Magic numbers and repeated literals

if (retries > 3) ...
// elsewhere
for (let i = 0; i < 3; i++) ...

The number 3 represents “maximum retry count” — a piece of knowledge. It should be a named constant.

4. Premature DRY / speculative abstraction

Creating a shared utility for two callsites that “might converge” when they actually represent different concepts. Results in an abstraction that satisfies neither, complicated with special-case parameters.

5. Diverged copies (WET drift)

Code that started as identical copies and has been updated inconsistently. One has the bug fix; the other doesn’t. Worse than never extracting, because the copies now actively disagree.



Part of the PushBackLog Best Practices Library. Suggest improvements →