Software Engineering

KISS, YAGNI and DRY: three stickers and what they really mean

Semi-skimmed a bit more body

Keep it simple, you aren’t gonna need it, don’t repeat yourself. Where the three most quoted acronyms in software come from (a spy plane, Extreme Programming and a 1999 book), what they really say, and what happens when they fight. With a farm, a milk price and no camels.

cowsay

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.

AcronymMeansBornReal meaningMisused as
KISSKeep it simple, stupidKelly Johnson, Lockheed Skunk WorksFewer moving parts, fewer ways to break“I don’t want to learn anything new”
YAGNIYou aren’t gonna need itExtreme Programming, late 1990sDon’t build features for a future you are guessing“No tests, no refactoring, no design”
DRYDon’t repeat yourselfThe Pragmatic Programmer, 1999Each 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 total

Same 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.

🐄
A factory with a registry for a single strategy that has a single implementation. Wrong. This isn’t architecture, it’s packaging. The milk is the same, but now it comes in five boxes, one inside the other.
🐄
Writing the five-line function, and adding the strategy the day you really have two pricing rules. Right. Simple code is easy to make complicated. The reverse almost never happens.
🐄
“Simple” according to whom? It depends. Simple doesn’t mean short, and it doesn’t mean “the first thing that came to mind”. A clever one-liner nobody can read isn’t simple. Simple means a colleague understands it on the first try.

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.

🐄
Supporting goats, camels and a blockchain in a farm that has two cows. Wrong. The cow checked: there are no camels in the barn. There never have been.
🐄
Deleting unused parameters and branches when you find them. Right. Version control remembers them for you. If the camels ever come, the code will be in the history, and you’ll write it better anyway, knowing what camels want.

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.

🐄
“We don’t write tests: YAGNI.” Wrong. Tests are exactly what lets you say YAGNI without fear. Without them it’s not YAGNI, it’s YOLO.

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.88

The 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.88
🐄
The milk price, the VAT rate, the size of a page: written in one place and imported everywhere. Right. When the same fact lives in two places, sooner or later they will disagree. And the customer will always find the cheaper one.

Now 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.

🐄
Merging two functions only because they look alike. Wrong. Two cows with the same spots are still two cows. Same code does not mean same knowledge.

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.

🐄
Copying and pasting a block of code. It depends. The second time is fine, with a wince. The third time, the cow expects a function. If it’s knowledge, like a price or a rule, once is already too many.

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).

🐄
Moral: keep it simple, because simple things break less and are easier to fix. Don’t build for the camels you imagine, but keep the code easy to change, for when they really arrive. And don’t repeat knowledge, but don’t fear a bit of repeated code. Stickers are for laptops, not for code reviews. The cow has spoken, and the cow is law.

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.

Enjoyed it? Follow the RSS feed and never miss a post. No email, no spam: cow’s honour.

RSS feed