Every developer’s laptop has at least one sticker with an acronym on it. Three of them are especially popular, and they sound like a fortune cookie: KISS, YAGNI and DRY. Keep it simple. You aren’t gonna need it. Don’t repeat yourself.
They are quoted in code reviews to win arguments, and almost always slightly wrong. So the cow went back to the sources: where each acronym comes from, what it really says, and what it doesn’t. With Python examples, all of them really run, and a farm that sells milk, which is the most serious business we know.
| Acronym | Means | Born | Real meaning | Misused as |
|---|---|---|---|---|
| KISS | Keep it simple, stupid | Kelly Johnson, Lockheed Skunk Works | Fewer moving parts, fewer ways to break | “I don’t want to learn anything new” |
| YAGNI | You aren’t gonna need it | Extreme Programming, late 1990s | Don’t build features for a future you are guessing | “No tests, no refactoring, no design” |
| DRY | Don’t repeat yourself | The Pragmatic Programmer, 1999 | Each piece of knowledge lives in one place | “Merge any two lines that look alike” |
KISS: keep it simple, stupid
KISS doesn’t come from software. It comes from aircraft. Its best-known source is Kelly Johnson, the engineer who founded Lockheed’s “Skunk Works”, the secret workshop that built the U-2 spy plane in 1955 and then the SR-71 Blackbird, the fastest and highest-flying aircraft of its time. In the memoir written in 1995 for the U.S. National Academy of Sciences by Ben Rich, who worked with him at the Skunk Works, Johnson’s sayings include “If it works, don’t fix it” and this one:
“Keep it simple, stupid—KISS—is our constant reminder.”
The phrase was in the air in those years: a dictionary of American slang records a U.S. Navy “Project KISS” as early as 1960. The point is the same. If a plane at Mach 3 can be kept simple, so can your invoice.
Speaking of invoices. Here is how to compute the price of 12.5 liters of milk at €1.20 a liter, written by someone who has just read a book on design patterns:
from abc import ABC, abstractmethod
class PricingStrategy(ABC):
@abstractmethod
def price(self, liters: float) -> float: ...
class StandardMilkPricingStrategy(PricingStrategy):
def __init__(self, rate: float):
self.rate = rate
def price(self, liters: float) -> float:
return round(liters * self.rate, 2)
class PricingStrategyFactory:
_registry = {"standard": StandardMilkPricingStrategy}
@classmethod
def create(cls, kind: str, **kwargs) -> PricingStrategy:
return cls._registry[kind](**kwargs)
class MilkInvoiceService:
def __init__(self, factory=PricingStrategyFactory):
self.strategy = factory.create("standard", rate=1.20)
def invoice(self, liters: float) -> float:
return self.strategy.price(liters)
print(MilkInvoiceService().invoice(12.5))And here is the same thing written by the cow:
def milk_price(liters, rate=1.20):
return round(liters * rate, 2)
print(milk_price(12.5))$ python3 over.py
15.0
$ python3 simple.py
15.0
$ wc -l over.py simple.py
33 over.py
5 simple.py
38 totalSame result, €15. One version has an abstract class, a strategy, a factory, a registry and a service: 33 lines to do one multiplication. The other has 5 lines. When the price changes, both need one edit. When there is a bug, though, one of them has five places to hide it.
YAGNI: you aren’t gonna need it
YAGNI was born in Extreme Programming at the end of the 1990s. Martin Fowler tells how it started: on the C3 project, Chet Hendrickson kept proposing capabilities the system would need one day, and each time Kent Beck answered “you aren’t gonna need it”. Ron Jeffries, another XP founder, summed it up in 1998 like this:
“Always implement things when you actually need them, never when you just foresee that you need them.”
Here is a milking function written by someone who “foresees” a lot:
def milk(animal, liters, species="cow", currency="EUR", blockchain=False, plugin=None):
if species == "goat":
raise NotImplementedError("goats: coming soon")
if species == "camel":
raise NotImplementedError("camels: phase 2")
if blockchain:
raise NotImplementedError("ask the investors")
if plugin is not None:
plugin.before_milking(animal)
return f"{animal}: {liters} L"Goats, camels, currencies, plugins and a blockchain. Let’s look at who really calls it, in the whole project:
$ grep -rn "milk(" --include=*.py . | grep -v "def milk"
./evening.py:3:print(milk("Ercolina", 11.0, species="cow", currency="EUR"))
./morning.py:3:print(milk("Ercolina", 12.5, species="cow"))
./morning.py:4:print(milk("Carolina", 9.0))Three calls. All of them for cows, all in euros, never a plugin, never a blockchain. The currency parameter isn’t even used inside the function. The honest version is this one:
def milk(animal, liters):
return f"{animal}: {liters} L"Fowler explains why the “just in case” version costs more than it seems, with four costs. The cost of build: the time spent writing, testing and documenting the camels. The cost of delay: what you didn’t build in the meantime, which customers were actually waiting for. The cost of carry: every unused parameter makes the code heavier to read and to change, forever. And the cost of repair: when the camels really arrive, the requirements will be different from what you guessed, and you’ll have to rewrite everything anyway. His example is an insurance startup for shipping that is working on storm risk and already knows that in six months it will need piracy risk: YAGNI says don’t build piracy now.
But be careful, because YAGNI is the most abused of the three. Fowler is explicit: YAGNI doesn’t apply to the work that makes code easier to change, such as tests, refactoring and clean design. On the contrary: YAGNI only works if the code is easy to change, because the whole bet is “we’ll add it later, when we need it”. If adding it later is a nightmare, the bet is lost.
DRY: don’t repeat yourself
DRY comes from The Pragmatic Programmer by Andy Hunt and Dave Thomas, 1999. The original definition is more precise than the sticker:
“Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.”
Note the word: knowledge, not code. Dave Thomas repeated it in a 2003 interview: “Most people take DRY to mean you shouldn’t duplicate code. That’s not its intention.” The knowledge of a system, he says, also includes database schemas, test plans, the build system and even the documentation.
Here is a real DRY violation. The milk price is written in two places. On Monday the price goes up to €1.35, and someone updates the invoice. The shelf label, not so much:
# invoice.py
def invoice_total(liters):
return round(liters * 1.35, 2) # new price since Monday
# shelf.py
def shelf_label(liters):
return f"{liters} L - EUR {liters * 1.20:.2f}"$ python3 main.py
shelf: 12.5 L - EUR 15.00
invoice: 16.88The customer reads €15.00 on the shelf and pays €16.88 at the till. That isn’t a style problem, it’s a complaint. The fix is to give the price one home:
# prices.py
PRICE_PER_LITER = 1.35 # the one and only place where the price lives
# invoice.py
from prices import PRICE_PER_LITER
def invoice_total(liters):
return round(liters * PRICE_PER_LITER, 2)
# shelf.py
from prices import PRICE_PER_LITER
def shelf_label(liters):
return f"{liters} L - EUR {liters * PRICE_PER_LITER:.2f}"$ python3 main.py
shelf: 12.5 L - EUR 16.88
invoice: 16.88Now the opposite case. These two functions are identical, character for character (we didn’t run them, they’re here to be looked at):
def valid_ear_tag(code):
return len(code) == 8 and code.isdigit()
def valid_invoice_number(code):
return len(code) == 8 and code.isdigit()Duplicated code! The DRY reflex says: merge them into a valid_8_digit_code(). But they aren’t the same knowledge. One is a rule about cow ear tags, the other one is a rule of the accounts office. Today they happen to coincide. The day the ear tag rules change, and the invoices don’t, the merged function grows a parameter. Then another. Then another:
def valid_code(code, *, is_ear_tag=False, country_prefix=False,
invoice_year=None, legacy_mode=False, vet_mode=False):
... # 60 lines of ifs. Nobody touches it. Nobody knows why.Sandi Metz described this story step by step in 2016, in “The Wrong Abstraction”. Programmer A sees duplication and extracts an abstraction. A new requirement arrives that almost fits, and programmer B adds a parameter and an if. Repeat until nobody understands anything anymore. Her conclusion has become famous: “duplication is far cheaper than the wrong abstraction”. And the cure is counterintuitive: put the duplicated code back where it was, in every caller, delete what each one doesn’t need, and only then look for the right abstraction.
How do you know when to merge? A good rule of thumb is the rule of three, which Martin Fowler reports in Refactoring (1999) as advice from Don Roberts: the first time you do something, you just do it. The second time you wince at the duplication, but you do it anyway. The third time, you refactor. With three examples in hand, it’s much easier to see what they really have in common.
When the three fight
The three acronyms don’t always agree. DRY pushes you to abstract; KISS and YAGNI push you not to build what you don’t need. The function with five extra parameters above is what happens when DRY wins too early: in order not to repeat yourself, you have built something that isn’t simple and that serves cases that may never come. In those cases, the cow’s order of precedence is this: first, does it work and can it be read (KISS); then, is it needed today (YAGNI); and only then, is there knowledge repeated (DRY).
Moo. 🐄
Sources
The Python examples were run with Python 3.13.5; the outputs shown are the real ones. The two validators and the function with flags are illustrative and were not run.
- Ben R. Rich, “Clarence Leonard (Kelly) Johnson 1910–1990”, Biographical Memoir, NAS (1995)
- Wikipedia, “KISS principle”
- Ron Jeffries, “You’re NOT gonna need it!” (1998)
- Martin Fowler, “Yagni” (2015)
- Wikipedia, “Don’t repeat yourself”
- Bill Venners, “Orthogonality and the DRY Principle”, interview with Dave Thomas (2003)
- Sandi Metz, “The Wrong Abstraction” (2016)
- Wikipedia, “Rule of three (computer programming)”