Case file — F3

Don't Repeat Yourself

Every fact has one home. The second copy is the bug.

Hunt and Thomas wrote it down in 1999 and the wording has not aged: every piece of knowledge must have a single, unambiguous, authoritative representation within a system. Two copies of the same fact mean the next maintainer changes one and the bug ships from the unchanged second. The regression resists git-blame because the lie was committed twice.

ByAdam LewisPublished30 Apr 2026Reading13 minVersionv1.0ConfidenceHigh
§0b

Opinion

I've watched DRY get applied as a reflex more than as a rule. Pragmatic Programmer readers extract a helper the moment they see two similar lines; six months later the helper carries flags for every caller and a name that no longer covers any of them. The fix is not to abandon DRY. The fix is to read the second sentence Hunt and Thomas wrote and notice the word knowledge.

Couple this with F4 Simplicity: extract on the second occurrence, never extract across a bounded context. Inside a context, deduplication is cheap insurance. Across one, the helper fuses two products into a single change.

Copy a note and link

Grab this short comment and drop it into a PR comment or an LLM chat to prompt the right change.

Each fact in the code should live in one place. If the same logic or value shows up two or three times, pull it into a single helper or constant. But don't merge code just because it looks the same — only merge if it means the same thing. Code that happens to look alike but represents different ideas should stay separate.

/tenet/dont-repeat-yourself/F3
§0c

AI eyes only

Rule: every fact has one home. Duplicate intent is debt.

Reject: copy-paste with one-line edits. Reject: near-duplicate helpers across modules. Reject: ad-hoc reformulations of utilities that already exist in lib/generic/ or lib/product/.

Generate: search the codebase for an existing helper before writing one. Reuse the canonical version. If no canonical version exists, extract on the second occurrence: agent diff cost has collapsed and waiting compounds the duplication.

Diagnostic: before emitting a function or constant, grep for the body. If a near-match exists, reuse it. Repeated literals are the same trigger as repeated logic.

§0d

Why?

  • One home for each fact: the next change is one diff, not a treasure hunt for siblings.
  • Bugs that come from forgetting to update the second copy disappear by construction — the second copy isn't there.
  • Reviewers see a small change to the canonical helper, not seven scattered edits in unrelated files.
  • Forces a name on the duplicated knowledge — feeds F2 Intention-Revealing Names by making the noun explicit.
  • Repetition is often a missing unit. Extracting reveals a single-purpose function or type that F1 would have demanded anyway.
  • One canonical implementation is one test surface — fewer redundant tests proving the same rule from six angles.
  • Renames, units changes, validation tweaks become cheap. The cost of correctness drops when correctness has one home.
The receipts
Origins, quoted passages, evidence, the strongest counter-argument and the reply.
§1

Origins

Hunt & Thomas coined the acronym in The Pragmatic Programmer (1999), tip 11: “every piece of knowledge must have a single, unambiguous, authoritative representation within a system.” The 20th-anniversary edition (2019) expands the wording but keeps the rule intact: DRY is about knowledge duplication, not text duplication.1Hunt & ThomasThe Pragmatic Programmer, 20th Anniversary Edition (Addison-Wesley, 2019). Tip 11 — “DRY: Don’t Repeat Yourself.” The original 1999 wording is preserved.

Kent Beck arrived at the same idea by a different route. The four rules of simple design from Extreme Programming Explained (1999), popularised by Martin Fowler, end with “no duplication” as the second strongest constraint after passing the tests.2Kent BeckFour rules of simple design — popularised by Martin Fowler at martinfowler.com/bliki/BeckDesignRules.html. Beck’s ordering puts no duplication third, after tests passing and intent revealed.

Robert C. Martin folds DRY into Clean Code (2008) as the structural counterpart to meaningful names: duplication is dishonest because it lies about which copy is the truth.3Robert C. MartinClean Code (Prentice Hall, 2008). Treats duplication as the principal cause of bad code; ties DRY to SRP via the missing-unit observation.

The most influential modern critique is Sandi Metz' 2014 RailsConf talk All the Little Things and her 2016 essay The Wrong Abstraction. The line “duplication is far cheaper than the wrong abstraction” reframes DRY as a tool, not a doctrine. Kent C. Dodds packages the corollary as AHA Programming: avoid hasty abstractions; prefer duplication until the third callsite shows the shape.4Sandi MetzThe Wrong Abstraction (sandimetz.com, 2016). Companion talk: All the Little Things (RailsConf 2014). Kent C. Dodds frames the corollary as AHA Programming.

Eric Evans draws the boundary in Domain-Driven Design (2003): across a bounded context, deduplication is harmful. Two contexts will diverge; fusing them creates a coupling that costs more than the duplication it deleted.5Eric EvansDomain-Driven Design (Addison-Wesley, 2003). Bounded contexts are the seam at which DRY stops applying.

§2

Quotes

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

Hunt & Thomas · The Pragmatic Programmer (1999), Tip 11

Duplication is far cheaper than the wrong abstraction. Prefer duplication over the wrong abstraction.

Sandi Metz · The Wrong Abstraction (2016)

Passes the tests · Reveals intention · No duplication · Fewest elements. In that order. Duplication ranks third because it is the easiest of the four to mistake for the others.

Kent Beck · four rules of simple design (via Martin Fowler)

Avoid Hasty Abstractions. Prefer duplication over the wrong abstraction and optimise for change first.

Kent C. Dodds · AHA Programming (2019)
§3

Evidence

Twenty external sources, ranked by author authority. The first five are the canon; expand to see the rest, including the qualifiers and the named opposers. Each links out to its primary source.

  1. 01
    Hunt & Thomas · 1999 / 2019
    The canonical statement of the rule. The 20e adds the gloss that DRY is about knowledge, not lines.
  2. 02
    Martin Fowler · 2018
    Mechanical recipes for collapsing duplication once it is real. Pair with Inline when the abstraction was wrong.
  3. 03
    Robert C. Martin · 2008
    Frames duplication as the root of bad code; ties DRY to SRP because the duplicated logic almost always reveals a missing unit.
  4. 04
    Kent Beck · ~2000
    Four rules of simple design — no duplication is third, after tests pass and intent is revealed.
  5. 05
    Sandi Metz · 2016
    The most cited counter to DRY puritanism. The right move when an abstraction is wrong is often to inline it back to duplication and start again.

Sixteen sources, four stances, one rule. The supporters trace from Hunt and Thomas in 1999 through Martin and Beck; Fowler catalogues the recipes. The qualifiers led by Metz install the brakes: respect the context boundary, distinguish coincidence from knowledge. The rule-of-three timing rule is named and refused below. The opposing “WET” line beneath the canon restates the qualifiers' case in stronger words. The rule survives the room.

§4

Examples

Viewing: TypeScript.
Avoid
Filebilling.ts
// Before: 0.20 lives in three places. One drift becomes a bug.function priceWithVat(net: number): number {  return net * 1.20;}function vatAmount(net: number): number {  return net * 0.2;}function isReducedRate(rate: number): boolean {  return rate < 0.20;}
Prefer
Filebilling.ts
// After: VAT_RATE is the single source of truth.const VAT_RATE = 0.20;function priceWithVat(net: number): number {  return net * (1 + VAT_RATE);}function vatAmount(net: number): number {  return net * VAT_RATE;}function isReducedRate(rate: number): boolean {  return rate < VAT_RATE;}
§4b

Enforcement

Viewing: TypeScript.

Apply these rules in .jscpd.json + eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.

RuleToolCatches
jscpdjscpdcopy-pasted blocks of 5+ lines / 50+ tokens across files — the duplication review forgets to flag.
sonarjs/no-identical-functionseslint-plugin-sonarjstwo functions with the same body and a different name — usually a missing helper or a missed rename.
sonarjs/no-duplicate-stringeslint-plugin-sonarjsthe same literal in three or more places — the constant you forgot to extract.
.jscpd.json + eslint.config.mjsconfiguration snippet
// jscpd.config.json — copy/paste detection in CI
{
  "threshold": 0,
  "min-tokens": 50,
  "min-lines": 5,
  "format": ["typescript", "tsx", "javascript", "jsx"],
  "ignore": ["**/*.spec.ts", "**/*.test.tsx", "**/__fixtures__/**"],
  "reporters": ["console", "html"],
  "absolute": true
}

// eslint.config.mjs — flag the same expression appearing twice
import sonarjs from 'eslint-plugin-sonarjs';

export default [
  sonarjs.configs.recommended,
  {
    rules: {
      'sonarjs/no-identical-functions': ['error', 3],
      'sonarjs/no-duplicate-string': ['error', { threshold: 3 }],
      'sonarjs/no-identical-expressions': 'error',
      'sonarjs/no-redundant-jump': 'error',
    },
  },
];
§4c

AI rules

File.cursor/rules/f3-dont-repeat-yourself.mdc
---
description: Prickles F3 — Don't Repeat Yourself
globs: "**/*.{ts,tsx,js,jsx,py,java,php}"
alwaysApply: false
---

## Prickles F3 — Don't Repeat Yourself

Every fact, rule, and decision in the system has one home. If you find yourself typing the same logic, the same literal, or the same shape twice, the second copy is a bug looking for a deadline.

Extract when the duplication represents the same knowledge. Leave it alone when the shapes look alike but the reasons differ — coincidental duplication is not duplication.

Constants over magic literals. A string that means the same thing in three places is a constant; a string that happens to spell the same letters in three places is a coincidence.

Cross a bounded context and DRY stops applying — duplicate the type, copy the helper, leave the seam visible. See F1 Single Responsibility for the unit boundary, A8 Bounded Contexts for the system boundary.

Refuse premature abstraction. Wait for the third occurrence (Rule of Three) before generalising; the wrong abstraction is more expensive than the duplication it replaced.

Repo layout, CI, and ESLint wiring for these paths live on /implementation — not repeated on every tenet.

§5

Counter-argument

Counter

The first steelman is Sandi Metz' The Wrong Abstraction: the cost of unwinding a bad abstraction is higher than the cost of the duplication it replaced, because the abstraction develops branches, flags, and second-meaning callsites that fuse unrelated decisions. Add bounded contexts and the rule appears to flip. Two services that share a User shape today will diverge tomorrow, and the “shared type” becomes a coupling neither team can refactor without scheduling a meeting.

The second steelman is the classic rule of three. Fowler attributes it to Don Roberts; Beck restated it in XP Explained as “three strikes and you refactor.” Extract too early and the abstraction is wrong; wait until the third caller lands and the shape of the helper is obvious.

§6

Counter-argument retort

Reply

The first steelman (Metz) is true, and it is not an argument against DRY: it is the second sentence of Hunt and Thomas' original tip, which most quotes drop. Knowledge, not text. If two callsites encode the same rule, collapse them; if they happen to share a body for now, leave them and rename the helper when the third caller reshapes the abstraction.

For bounded contexts the answer is also in the canon: cross a context, duplicate the type. The cost of the duplication is paid once and it is bounded; the cost of the coupling compounds for the life of the system. A8 carries that load deliberately so F3 does not have to.

The rule-of-three steelman is acknowledged and refused. Don't wait for three. The Fowler/Beck argument is an artefact of an era when the cost of detecting and renaming a helper was high. With AI agents reading the diff before the human does, that cost has collapsed. Waiting for the third caller is now waiting while the duplication compounds, the helper grows three subtly different copies, and the bug ships in two of them. Extract on the second occurrence; if the third caller reshapes it, the agent renames the helper in seconds. The wrong abstraction is still cheap to fix; the duplication that was missed is not.

Default stance for code inside a single context: extract on the second occurrence, refuse the magic literal the moment it repeats, accept that the right number of times to write a rule is one. Everything else is an exception that gets defended in the PR description.

§7

Notes

  1. [1]Hunt & ThomasThe Pragmatic Programmer, 20th Anniversary Edition (Addison-Wesley, 2019). Tip 11 — “DRY: Don’t Repeat Yourself.” The original 1999 wording is preserved.
  2. [2]Kent BeckFour rules of simple design — popularised by Martin Fowler at martinfowler.com/bliki/BeckDesignRules.html. Beck’s ordering puts no duplication third, after tests passing and intent revealed.
  3. [3]Robert C. MartinClean Code (Prentice Hall, 2008). Treats duplication as the principal cause of bad code; ties DRY to SRP via the missing-unit observation.
Disagree? Found a hole in the argument? Take issue with this tenet →
Last revised: 2026-04-27