open atlas
↑ Back to track
Code patterns & craft CP · 12 · 01

DRY, KISS, YAGNI in tension

DRY, KISS, and YAGNI are heuristics, not laws, and they pull against each other. DRY targets one source of knowledge — not identical-looking text. Two fragments that change for different reasons are not duplication, and DRY-ing them couples unrelated things.

CP Senior ◷ 20 min
Level
FoundationsJuniorMiddleSenior

You have three acronyms taped above your desk: DRY, KISS, YAGNI. Each is repeated in code review as if it settled the argument. Then a reviewer says “this is duplicated, DRY it up,” you extract a shared helper, and three weeks later that helper has a mode flag, then an isLegacy boolean, then a skipValidation option — one branch per caller. You followed the rule and produced a mess.

That happens because these three are not laws you apply mechanically. They are heuristics, and worse, they conflict: DRY pushes you to unify, KISS and YAGNI push you to leave things alone and apart. The senior skill is not memorising them — it is deciding which one dominates in this spot, and knowing the failure mode each one creates when it wins where it shouldn’t.

Goal

After this lesson you can state precisely what DRY protects — one authoritative source for a piece of knowledge, not for identical-looking text; distinguish true duplication (same reason to change) from coincidental duplication (same shape, different reasons); apply KISS and YAGNI as the counterweights that keep DRY from over-abstracting; and recognise the wrong-abstraction failure mode — a shared helper that sprouts a boolean flag per caller — before you ship it.

1

DRY is about knowledge, not text. The “Don’t Repeat Yourself” principle, as originally stated, is: every piece of knowledge must have a single, authoritative representation in the system. The unit is a piece of knowledge — a business rule, a calculation, a policy — not a run of characters that happen to look the same. Two snippets can be byte-for-byte identical and still not be a DRY violation, because they encode two different pieces of knowledge that merely coincide today.

// Both compute "is this value present and non-empty". Same TEXT.
function isValidUsername(s: string): boolean {
  return s != null && s.trim().length > 0;
}
function isValidComment(s: string): boolean {
  return s != null && s.trim().length > 0;
}

These look like duplication. But “what makes a username valid” and “what makes a comment valid” are two separate rules that happen to agree right now. The moment usernames need a max length and comments need a profanity filter, the shared version fractures. DRY does not apply here, because there is no single piece of knowledge — there are two.

2

The real test of duplication is: do these change for the same reason? This is the operational form of DRY. If a single decision in the business would force you to edit both fragments together, they encode one piece of knowledge and belong in one place. If a change to one would leave the other correct, they are independent and must stay apart.

// TRUE duplication: one rule, the 20% VAT rate, written twice.
const invoiceTotal = subtotal * 1.2;      // in invoice.ts
const quoteTotal   = estimate * 1.2;      // in quote.ts

Here the 1.2 is the same fact — the VAT rate — expressed twice. When VAT changes to 22%, both must change, together, for the same reason. That is true duplication: name it once (vatRate, applyVat()) and both sites depend on the one source. The literal looking identical is incidental; what matters is that one decision drives both. Same shape can be coincidence; same reason to change is duplication.

3

KISS and YAGNI are the counterweights that stop DRY from over-firing. DRY, taken alone, has an obvious failure direction: unify aggressively and you couple things that were independent. KISS (“keep it simple — the simplest thing that works”) and YAGNI (“you aren’t gonna need it — don’t build for needs you don’t have yet”) pull the other way. They are not in opposition to DRY by accident; they exist precisely to bound it.

// YAGNI violation born from "DRY-ing ahead": a generic engine for ONE caller.
function process<T>(
  items: T[],
  opts: { parallel?: boolean; retries?: number; transform?: (x: T) => T; dedupe?: boolean },
): T[] { /* 60 lines of branching nobody currently exercises */ }

// What was actually needed today:
const result = items.map(normalize);

The generic process is “DRY” in the sense that it could absorb future callers — but none of those callers exist. KISS says: write the .map(normalize). YAGNI says: don’t build the options bag for needs you can’t yet name. The flexibility is a guess, and every branch is code to read and a place for bugs while it sits unused.

4

The failure mode: DRY-ing coincidental duplication produces the wrong abstraction — a shared helper that sprouts a flag per caller. This is the most expensive of the three failures because it looks like good work. You see two similar blocks, extract a shared() helper, feel virtuous. Then caller A needs a slightly different behaviour, so you add a boolean. Caller B needs another tweak, so you add a second boolean. Each caller passes its own combination of flags; the helper’s body becomes a thicket of if (mode === ...). You now have something worse than the duplication you removed: a single function coupling unrelated callers, where a change for caller A risks breaking caller B.

// The wrong abstraction, fully grown:
function renderRow(item: Item, opts: {
  asAdmin?: boolean; compact?: boolean; forEmail?: boolean; legacyV1?: boolean;
}): string {
  // every combination of flags is a different caller's real requirement,
  // tangled into one body. Changing the email path can break the admin path.
}

Sandi Metz’s rule of thumb captures the escape: prefer duplication over the wrong abstraction. When an extracted helper starts growing per-caller flags, that is the signal the callers were never the same knowledge. The cheaper move is to inline it back — re-duplicate — and let each caller be simple and independent again. Duplication is far cheaper to fix later than a wrong abstraction everyone now depends on.

Worked example

A reviewer says “DRY this.” Decide whether they’re right. Two endpoints validate an incoming amount:

// payments.ts
function validatePaymentAmount(cents: number): boolean {
  return Number.isInteger(cents) && cents > 0 && cents <= 1_000_000;
}

// refunds.ts
function validateRefundAmount(cents: number): boolean {
  return Number.isInteger(cents) && cents > 0 && cents <= 1_000_000;
}

Identical bodies. The naive move is to extract validateAmount(cents) and call it from both. Before doing that, apply the real test: will a payment-rule change and a refund-rule change always be the same change? Almost certainly not. Payments will grow a per-merchant ceiling; refunds will grow a “can’t exceed the original charge” rule. They look the same today by coincidence. Extracting now creates a shared function that the very next requirement will fork — with a flag:

// What the premature extraction becomes after one real requirement:
function validateAmount(cents: number, kind: "payment" | "refund"): boolean {
  const base = Number.isInteger(cents) && cents > 0 && cents <= 1_000_000;
  if (kind === "refund") return base; // ...and soon: && cents <= originalCharge
  return base;                        // ...and soon: && cents <= merchantCeiling
}

The kind flag is the tell: one helper now serves two unrelated rules, and each future change to one risks the other. The correct call here is leave them duplicated — the bodies agreeing is coincidence, and KISS/YAGNI win. Contrast that with the VAT case from Step 2, where the 1.2 is one fact: there, the reviewer is right and DRY wins. Same surface symptom (“duplicated code”), opposite correct decisions — because the deciding question is the reason to change, not the appearance.

Why this works

Why frame these as forces in tension rather than a checklist? Because a checklist implies you can satisfy all of them at once, and you frequently can’t. Removing the last duplication (max DRY) tends to add indirection and coupling (anti-KISS). Building the fully general solution (anti-YAGNI) is often the most DRY. The principles encode the costs of opposite extremes — scattered knowledge on one side, premature coupling and speculative generality on the other. Judgment is reading which cost is larger here: how confident are you these two things are truly one piece of knowledge, and how expensive is being wrong in each direction? That weighing is the work; the acronyms are just names for the two ends of the scale.

Common mistake

The most common senior-level misfire is treating DRY as “no two lines may look alike” and reaching for an abstraction at the first repeated shape. Repetition of appearance is cheap and often harmless; repetition of knowledge is the real defect. Inverting that — deduplicating text while ignoring whether the knowledge is shared — is exactly what manufactures the wrong abstraction. If you find yourself adding a boolean parameter so one helper can serve two callers, stop: that parameter is evidence the callers were never the same thing. Inlining back to two simple functions is not a regression; it is the fix.

Check yourself
Quiz

Two functions have byte-identical bodies today. A reviewer says 'DRY violation, extract a shared helper.' What single question best determines whether they're right?

Recap

DRY, KISS, and YAGNI are heuristics that pull against each other, and judgment is deciding which dominates here. DRY protects one authoritative source for a piece of knowledge — not for identical-looking text — and its operational test is “do these change for the same reason?” KISS (simplest thing that works) and YAGNI (don’t build ahead of real need) are the counterweights that stop DRY from coupling independent things and from generalising for callers that don’t exist. The signature failure is DRY-ing coincidental duplication into a shared helper that then grows a boolean flag per caller — the wrong abstraction, which is more expensive than the duplication it replaced; the escape is to prefer duplication and inline it back. None of these is a law: the senior move is to ask, for each repeated shape, whether it is one piece of knowledge or two that merely coincide.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 4 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.