core/principles/dry_kiss_yagni.md

DRY, KISS, and YAGNI: Balancing Reuse, Simplicity, and Restraint

Sandi Metz's 'wrong abstraction' pattern, Kent C. Dodds's AHA alternative to DRY/WET, and the correctly-attributed Rule of Three, with the concrete failure signatures that tell you which side of the DRY tension you're actually on.

Assume the reader knows what DRY/KISS/YAGNI stand for. What matters is the specific, named failure pattern each one produces when misapplied, and the concrete signature that tells you which failure mode you're looking at.

The Wrong Abstraction (Sandi Metz)

Sandi Metz's "The Wrong Abstraction" (sandimetz.com/blog/2016/1/20/the-wrong-abstraction, originating in her RailsConf 2014 talk) names the specific mechanism by which premature DRY-ing goes bad, not just the fact that it can:

  1. Programmer A sees duplication, extracts it, gives it a name. A clean abstraction exists.
  2. A new requirement arrives that the abstraction almost fits. Programmer B, reluctant to duplicate now that a shared version exists, adds a parameter and a conditional branch to make the old code serve the new case too.
  3. Repeat with programmer C, D. The abstraction accretes flags and branches keyed on the caller until it no longer represents one coherent concept - it represents N unrelated concepts wearing one name.

Her rule, verbatim and worth keeping verbatim: "duplication is far cheaper than the wrong abstraction." Her prescribed fix is not "avoid abstraction" - it's that once an abstraction is proven wrong, re-introduce duplication deliberately (inline the shared function back into each call site) and let the real, now-visible variation points tell you what the correct shared shape actually is. "When the abstraction is wrong, the fastest way forward is back."

Failure signature that tells you you're inside this trap right now: a shared function/class gaining a boolean parameter, a caller_type check, or an if/else branch that exists only to keep one specific caller happy. That is not a call for more cleverness inside the abstraction - it is the signal to split it back apart.

# Wrong-abstraction smell: flags multiplying with every new caller
def send_notification(user, message, is_urgent=False, skip_email=False, use_sms_only=False):
    ...

# Correct response: split back into explicit, named call sites
def send_urgent_sms_alert(user, message): ...
def send_standard_email_notification(user, message): ...

Note the debate did not end with Metz: codewithjason.com and terriblesoftware.org ("Duplication Is Not the Enemy," 2025) push back that "prefer duplication" is itself sometimes cited defensively to avoid ever committing to a needed abstraction, letting real duplicated business rules drift out of sync silently. Treat Metz's rule as describing a specific, recognizable failure mode (accreting flags), not a blanket license to never abstract.

AHA: Avoid Hasty Abstractions (Kent C. Dodds)

Kent C. Dodds's AHA (kentcdodds.com/blog/aha-programming) is presented explicitly as neither DRY nor WET but a mindset that replaces the mechanical trigger (three occurrences → abstract) with a confidence trigger: stay duplicated until you're confident you understand the actual use cases the duplication represents, and let the real commonalities "scream at you" for the abstraction rather than abstracting on schedule. This matters because the classic Rule of Three is a counting heuristic - AHA's point is that occurrence count and understanding are not the same thing; you can hit three occurrences and still not understand the shape, or understand it clearly on the second.

Correct Attribution: The Rule of Three Is Not GoF

The Rule of Three ("duplicate once without guilt, wince the second time, refactor the third") is commonly and incorrectly attributed to the Gang of Four. Its actual, traceable origin is Don Roberts, credited by Martin Fowler in Refactoring (1999): "Here's a guideline Don Roberts gave me: the first time you do something, you just do it. The second time you do something similar, you wince at the duplication, but you do the duplicate thing anyway. The third time, you refactor." Roberts and Ralph Johnson's earlier "Evolving Frameworks: A Pattern Language for Developing Object-Oriented Frameworks" (1996) traces it further back to Ted Biggerstaff's "3-system rule" from a 1988 workshop. Cite Fowler/Roberts, not GoF, if attribution matters in what you write.

Decision Table

SituationWhich failure mode appliesAction
Shared function just grew a boolean/flag parameter for a new callerWrong Abstraction (Metz)Inline it back into call sites; re-abstract once the real shape is visible
Two blocks look identical but you can't yet name the shared concept preciselyPremature DRY / AHA violationDuplicate (WET) until confident, do not extract on sight
You're refusing to add a single-implementation interface at a real architectural seam (persistence, external API) because "YAGNI"YAGNI misapplied to boundaries, not varietyAdd the seam - it costs almost nothing and pays for itself the first time you need to fake it in a test. YAGNI opposes speculative variety (a second implementation you don't have), not a boundary (one interface, one implementation)
"Simple" is being used to justify skipping tests/error handlingKISS misapplied to correctnessKISS constrains design complexity, not robustness. The simplest correct, tested design is the target - not the fewest lines
Test setup duplicated 3+ timesLegitimate DRY caseExtract a fixture/factory - test setup usually is one piece of shared knowledge
Test assertions/readabilityNot a DRY caseKeep assertions explicit per test; a shared assertion helper hides the exact thing a failing test needs to show you

Where DRY Is Almost Always Correct (No Rule-of-Three Wait Needed)

Cross-cutting infrastructure concerns - auth checks, logging format, request-id propagation - represent one real piece of knowledge from day one. There's no "wait and see if it diverges" here the way there is with business rules; unlike business rules, these concerns are defined by the platform, not by domain judgment calls that legitimately differ per caller.

Where Duplication Should Be Kept Deliberately

  • Different bounded contexts (DDD sense): a Customer in a billing module and a Customer in a support module that look similar are usually two different models of the same real-world entity, not a DRY violation. Merging them creates coupling between contexts that should stay independent.
  • Feature-flagged rollouts: an old and new code path coexisting behind a flag is deliberate, time-boxed WET - acceptable because the duplication has an explicit expiration (flag removal), unlike accidental duplication with no end date.
  • Public library/SDK boundaries: the one place speculative generality (versioned interfaces, extension points) is justified ahead of demonstrated need, because changing a published API after release is far more expensive than the cost of the unused abstraction would have been.
  • SOLID Principles - Open/Closed and Dependency Inversion are the correct seam once Metz's "real variation" is actually discovered, not before.
  • Clean Code Principles - naming/function-shape heuristics for telling true duplication from coincidental similarity.

Sources