Beginner15 min readDesign Principles & Heuristicslive prototype

DRY, KISS, YAGNI

Three everyday rules of thumb that keep code clean. DRY: every fact lives in one place. KISS: the simplest thing that works beats the clever thing. YAGNI: don't build for a future you only imagine. Handy on their own — and most useful when you feel the tension between them.

The idea

What it is

DRY, KISS, and YAGNI are three short rules of thumb. They don't need a framework or a diagram. They fit on a sticky note. Yet they catch a huge share of the mess that creeps into real code. Learn them once and you'll spot trouble before it's written.

Think of writing code like writing down the rules of a board game. DRY says: write each rule once, in one place — if you write 'a turn lasts 60 seconds' on three different pages, someone will change one and forget the others. KISS says: explain the rules so a new player gets it fast — clever phrasing that needs re-reading three times is worse than a plain sentence. YAGNI says: don't write rules for a tournament mode nobody asked for yet — you'll spend pages on a game variant that never gets played.

One line each

DRYDon't Repeat Yourself: every piece of knowledge has exactly one home. KISSKeep It Simple: the simplest thing that works wins. YAGNIYou Aren't Gonna Need It: build what today needs, not what tomorrow might.

They can pull against each other

These rules are friends most of the time, but they argue at the edges. Chase DRY too hard and you'll merge two things that only looked alike, creating a tangled abstraction that breaks KISS. The honest rule of thumb: prefer a little duplication over the wrong abstraction. We'll come back to this — it's the most important idea in the lesson.

Mechanics

How it works

DRY — every fact has one home

DRY isn't really about lines of code looking the same. It's about knowledge. A single fact — a tax rate, a validation rule, the shape of a database row — should have one authoritative place it lives. When that fact changes, you change it in one spot, and the whole system stays correct.

The classic smell is the same calculation copy-pasted around the codebase:

typescript
// ⚠️ same tax knowledge, pasted in three places
function cartTotal(p: number)    { return p + p * 0.08; }
function invoiceTotal(p: number) { return p + p * 0.08; }
function receiptTotal(p: number) { return p + p * 0.08; }

The day the tax rate becomes 9%, you must remember all three spots. Miss one and your receipts disagree with your invoices. DRY says: give that fact one home.

typescript
const TAX_RATE = 0.08;                 // the single source of truth
function withTax(p: number) { return p + p * TAX_RATE; }
// cartTotal, invoiceTotal, receiptTotal all call withTax()

DRY is about knowledge, not characters

Two lines of code that look identical today but mean different things will drift apart tomorrow. A shipping fee that happens to equal the tax rate isn't the same fact — merging them is a bug waiting to happen. Only deduplicate things that are the same knowledge, not things that merely look alike.

KISS — simple beats clever

Code is read far more often than it's written. KISS says the best version of a piece of code is the one the next person understands fastest — usually you, six months from now. A clever one-liner that saves three characters but takes a minute to decode is a bad trade.

typescript
// clever — correct, but you have to stop and decode it
const r = a.reduce((s, x) => s + (x.q ? x.q * x.p : 0), 0);

// simple — boring, and instantly readable
let total = 0;
for (const item of cart) {
  if (item.quantity) total += item.quantity * item.price;
}

Both compute the same total. The second one is longer and that's fine — anyone can read it at a glance, fix it under pressure, and trust it. KISS isn't 'write less code'; it's 'write code that's easy to understand.'

YAGNI — build for today, not for a guess

YAGNI fights the urge to add things 'just in case'. You're asked to save a user's name. The simple feature is one text field and a save. But it's tempting to also add multi-currency support, a plugin system, and full internationalization — because maybe the product will need them someday.

YAGNI says: don't. Every speculative feature is code you have to write, test, document, and maintain now, for a payoff that may never come. Most guessed-at futures never arrive, and when the real need shows up it's usually different from what you imagined — so you'd rebuild it anyway. Build the thing in front of you.

The classic trap: premature abstraction & speculative generality

The deepest mistake behind all three is building for an imagined future. Speculative generality is adding hooks, config flags, and abstract base classes for cases that don't exist yet. Premature abstraction is wrapping code in a flexible framework before you understand the real shape of the problem. Both feel responsible. Both usually produce guesswork you'll delete later. Wait until you have two or three real examples before you abstract — then you'll abstract the right thing.

The tension: DRY vs. the wrong abstraction

Here is where these rules collide, and where good judgement matters most. Suppose two functions look 90% identical. DRY screams: merge them! So you extract a shared helper. Then the two callers slowly diverge — one needs an extra step, the other a special case. You add a flag parameter. Then another. Soon the 'shared' function is a maze of ifs that serves neither caller well. You optimized for DRY and destroyed KISS.

Two bits of code that look 90% alike force DRY a little duplication one shared function they start to diverge… a maze of if(flag) the WRONG abstraction — serves neither caller two small functions each free to change stays simple (KISS) if they later prove the SAME, merge then
When two pieces of code merely look alike, forcing them under one DRY abstraction often backfires: they diverge, you bolt on flag parameters, and you end up with the wrong abstraction that breaks KISS. Keeping a little duplication leaves each piece simple and free to evolve — and if they turn out to be the same knowledge, you can always merge them later.

Sandi Metz: "prefer duplication over the wrong abstraction"

This trade-off is famous enough to have a name: AHA — Avoid Hasty Abstractions. The guidance: don't deduplicate the moment you see repetition. Wait until the duplication has proven it's the same knowledge (often after the third occurrence). A little duplication is cheap to fix; the wrong abstraction is expensive to unwind. When DRY and KISS disagree, lean toward simple.

Interactive prototype

See it. Build it. Break it.

A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.

About this simulation

Three tabs, one rule each, with a single explain panel replaced on every click. DRY shows the same tax formula copy-pasted into three places; press Extract shared function and they collapse into one home while a places to change counter drops 3 → 1. KISS toggles between a cryptic one-liner and a plain readable version of the same logic, with a seconds to understand meter that improves for the simple one. YAGNI lists features for 'save a user's name' — tap the speculative ones (multi-currency, plugin system, i18n) to defer them and watch the code to build now meter fall. Nothing scrolls; the panel narrates each move.

Hands-on

Try these yourself

Open the prototype above, predict what happens, then verify.

try 01

DRY — give the fact one home

Open the DRY tab. You'll see the same price + price * 0.08 tax formula copy-pasted into three functions, and a places to change when the tax rate moves counter reading 3. Press Extract shared function. The three copies collapse into one withTax() everyone calls, and the counter drops to 1. The explain panel makes the point: one fact, one home — change it once and the whole app stays correct.

try 02

KISS — simple beats clever

Switch to the KISS tab. A toggle flips the same logic between a cryptic reduce one-liner and a plain for loop. Watch the seconds to understand meter: the clever version reads high (slow to decode), the simple version reads low (instant). Both produce the same answer — KISS just picks the one the next reader grasps fastest.

try 03

YAGNI — defer the speculative

Open the YAGNI tab. The job is save a user's name, but the checklist is padded with guessed-at features: multi-currency, a plugin system, full i18n. Click each speculative item to defer it and watch the code to build & maintain now meter fall. What's left is the one real feature today actually needs. Build that; let tomorrow ask for the rest.

In practice

When to use it — and what trips people up

Reach for each rule when…

  • DRY — when you catch yourself copy-pasting a fact (a rate, a regex, a config value, a row shape) into a second or third place. Give it one home before the copies drift apart.
  • KISS — always, but especially when you feel the pull to be clever: a dense one-liner, a too-flexible config, a fancy pattern. Ask 'will the next reader get this fast?' If not, simplify.
  • YAGNI — when you're about to add something 'just in case' for a future nobody has actually asked for. Build the requirement in front of you; revisit when the real need shows up.

When DRY and KISS fight, KISS usually wins

If removing duplication would force you to add flags, special cases, or a confusing abstraction, stop. A little honest duplication is easier to read and cheaper to change than a tangled 'shared' helper. Wait for the duplication to prove it's truly the same knowledge — then deduplicate with confidence.

What it gives you

  • Fewer bugs from drift — DRY means a fact changes in exactly one place, so copies can't silently disagree.
  • Easier to read and fix — KISS code is graspable under pressure by whoever maintains it next (often future-you).
  • Less wasted work — YAGNI keeps you from building, testing, and maintaining features that never ship.
  • Smaller surface area — simpler code with one home per fact has fewer places for bugs to hide.

Common mistakes

  • DRY misapplied creates the wrong abstraction — merging things that only look alike couples them and adds flag-driven complexity.
  • KISS can be used as an excuse to skip genuinely needed structure — 'simple' must not mean 'naive' or 'unsafe'.
  • YAGNI taken too far skips real, known near-term needs — it's about speculative features, not deliberately ignoring the roadmap.
  • They're heuristics, not laws — each requires judgement, and the three sometimes point in different directions.

Reference

Code & further reading

A minimal reference implementation and pointers worth bookmarking.

// ===== DRY — one home for the tax fact =====
// BEFORE: same knowledge pasted in three places.
function cartTotalV0(p: number)    { return p + p * 0.08; }
function invoiceTotalV0(p: number) { return p + p * 0.08; }
// AFTER: single source of truth — change it once.
const TAX_RATE = 0.08;
function withTax(p: number) { return p + p * TAX_RATE; }
const cartTotal    = (p: number) => withTax(p);
const invoiceTotal = (p: number) => withTax(p);

// ===== KISS — simple beats clever =====
type Item = { quantity: number; price: number };
// clever one-liner (you have to decode it):
const totalClever = (cart: Item[]) =>
  cart.reduce((s, x) => s + (x.quantity ? x.quantity * x.price : 0), 0);
// simple, instantly readable — and that's the point:
function total(cart: Item[]) {
  let sum = 0;
  for (const item of cart) {
    if (item.quantity) sum += item.quantity * item.price;
  }
  return sum;
}

// ===== YAGNI — build only what today needs =====
// The ask: "save a user's name." So just do that.
function saveName(name: string) { db.save({ name }); }
// DON'T pre-build a speculative future nobody asked for:
//   multi-currency, a plugin system, full i18n…
//   add them when a real requirement actually arrives.
declare const db: { save: (row: { name: string }) => void };

References & further reading

7 sources

Knowledge check

Did it land?

Quick questions, answers revealed on submit. Sign in to save your best score.

question 01 / 05

What does DRY (Don't Repeat Yourself) actually ask you to avoid duplicating?

question 02 / 05

A teammate replaces a clear 6-line loop with a dense one-line reduce that does the same thing. From a KISS standpoint, is that an improvement?

question 03 / 05

You're asked only to 'save a user's name,' but you're tempted to also add a plugin system and multi-currency support for later. What does YAGNI advise?

question 04 / 05

Two functions look 90% alike, so you merge them under one shared helper. Over time you keep adding flag parameters and special cases until it's a maze. What went wrong?

question 05 / 05

What is 'speculative generality' and which rule most directly guards against it?

0/5 answered