Appearance
9.3.3 — KISS: Keep It Simple
Here is a real function from the delivery app. Its job is to decide whether an order qualifies for free delivery.
typescript
const RULES: Array<[predicate: (o: Order) => boolean, weight: number]> = [ // (1)
[(o) => o.total.gte(Money.of(50000)), 3],
[(o) => o.customer.isPrime, 2],
[(o) => o.distance.km < 2, 1],
];
function qualifiesForFreeDelivery(order: Order): boolean {
return RULES
.reduce((acc, [p, w]) => acc + (p(order) ? w : 0), 0) >= 3; // (2)
}Line (1) builds a list of pairs: a test, and a number of points that test is worth. Line (2) adds up the points for every test that passes and checks whether the total reaches three.
It is clever. It is compact. It is also the function that cost a team four hours.
A bug report said a customer with a 480-rupee order, one kilometre away, was not getting free delivery. Reading the code, you have to hold three things in your head at once: what each test does, what each weight means, and what the threshold of 3 implies about which combinations qualify. Then you work out that this customer scores 1, and 1 is less than 3, so no free delivery. Then you ask the real question — is that what the business wanted? — and the code cannot tell you, because the business rules were never written down anywhere in words. They were compiled into weights.
Here is the same behaviour, written the boring way:
typescript
function qualifiesForFreeDelivery(order: Order): boolean {
if (order.total.gte(Money.of(50000))) return true; // (1)
if (order.customer.isPrime && order.distance.km < 2) return true; // (2)
return false; // (3)
}Line (1) says orders over five hundred rupees get free delivery. Line (2) says members within two kilometres do too. Line (3) says nobody else does.
Now read it against the bug report. The customer spent 480, so line (1) does not apply. They are not a member, so line (2) does not apply. No free delivery, and you knew that in four seconds. More importantly, you can now show these three lines to the person who owns the business rules, and they can tell you whether they are right — which they could never have done with the weights version.
The clever version and the boring version do the same thing. One of them can be checked by a human.
1. What "simple" actually means
The slogan is "keep it simple, stupid", supposedly from an aircraft engineer whose planes had to be repairable in a field with basic tools. The trouble with the slogan is that "simple" sounds like a matter of taste, so arguments about it go nowhere.
Rich Hickey drew the distinction that makes it arguable, and it is worth learning because it changes what you look for.
Simple is the opposite of complex, and it means "one strand, not braided together". A thing is simple when it does one job and is tangled with nothing else. This is an objective property — you can point at the strands.
Easy is the opposite of hard, and it means "familiar, near to hand". This is about you, not the code. Something is easy because you have seen it before.
They come apart constantly, and that is the whole insight. The weighted-rules version above is easy for the person who wrote it — they hold the model in their head, so it reads fine. It is not simple: the business rules, the scoring mechanism and the threshold are braided into one expression, and you cannot examine one without the others. The boring version is simple, because each rule stands alone, and it is also easy, because if is familiar to everyone.
So when you argue about this in review, do not say "this is too complicated", which sounds like an opinion. Say "the business rule and the scoring mechanism are tangled here, so I cannot check the rule without also reasoning about the weights." That is a fact about the code.
2. What complexity actually costs
Complexity is not paid at writing time. It is paid every time somebody reads, changes or debugs the code, and those happen far more often (9.1 section 1).
Three specific costs, so you can name them:
Reading cost. Every level of nesting or indirection adds something the reader must hold in their head while continuing. Working memory is small. When code demands more than a handful of live facts at once, understanding stops being reliable and people start guessing.
Change cost. To change something safely you must first predict the effect. In tangled code you cannot, so you either test exhaustively or you ship a guess. Both are expensive, and the second is how bugs arrive.
Onboarding cost. A new engineer becomes productive at the speed they can build a working picture of the system. Clever code makes that slow, and it makes them afraid to touch things, which is worse.
There is a specific version of this worth calling out, because it stops a common argument: your own cleverness expires. The person who cannot read your clever code in six months is usually you. Nobody plans for this, and everybody experiences it.
3. The moves that make code simpler
Six concrete moves. Each is small, and together they cover most of what "simplify this" should mean in a review.
One, flatten with early returns. Nesting costs the reader more than any other single thing, because each level is one more condition they must keep live:
typescript
// ❌ three conditions held at once, all the way down
function ship(order: Order): void {
if (order.isPaid) {
if (order.items.length > 0) {
if (!order.isShipped) {
dispatch(order);
}
}
}
}
// ✅ each condition dealt with and forgotten
function ship(order: Order): void {
if (!order.isPaid) return;
if (order.items.length === 0) return;
if (order.isShipped) return;
dispatch(order);
}Same logic, same number of branches. The second one lets the reader discharge each fact and drop it, instead of carrying all three to the bottom.
Two, name the intermediate step. A long expression forces the reader to evaluate it in their head. A named variable tells them the answer:
typescript
// ❌ what is this asking?
if (o.items.some((i) => i.category === "alcohol") && o.customer.age < 21) reject();
// ✅ the names are the explanation
const hasAlcohol = o.items.some((i) => i.category === "alcohol");
const isUnderage = o.customer.age < 21;
if (hasAlcohol && isUnderage) reject();The second version costs two lines and removes the need to think.
Three, one level of detail per function. If a function mixes business steps with byte-level fiddling, the reader changes altitude every line, which is exhausting even when the function is short (9.2.3 section 7).
Four, replace a clever trick with the obvious version. ~~x instead of Math.floor(x), a bitmask instead of three booleans, a regular expression where a startsWith would do. Each saves characters and costs comprehension. Reach for the trick only when a measurement proves you need it, and then leave a comment explaining what it does.
Five, delete the case that never happens. Options nobody uses, branches for inputs that cannot occur, a configurable value that has never been configured. Every one of them is complexity you maintain for nothing. This is where KISS and YAGNI (9.3.4) meet.
Six, choose the boring tool. A Map instead of a custom index. A plain array instead of a tree, for two hundred items. An if instead of a lookup table, for three cases. Fancy structures earn their place at a scale most code never reaches.
4. Simple is not the same as short
This is the misreading that does the damage, so it gets its own section.
typescript
// Short. Not simple.
const t = o.i.reduce((a, c) => a + c.p * c.q, 0) * (1 + (o.c.t ?? 0.18));
// Longer. Simple.
const subtotal = order.items.reduce(
(sum, line) => sum.plus(line.unitPrice.times(line.quantity)),
Money.zero(),
);
const taxRate = order.customer.taxRate ?? DEFAULT_TAX_RATE;
const total = subtotal.plus(subtotal.times(taxRate));The first is one line and takes a minute to decode, because every name is a single letter, two ideas are fused, and the default tax rate is a magic number sitting inside an expression. The second is six lines and takes five seconds, because each line does one thing and says what it is.
Fewer lines is not the goal. Fewer things the reader must work out is the goal. Sometimes that means more lines, and that is fine.
The same warning applies to removing functions. Splitting one clear 40-line function into five 8-line functions that all read and write the same shared state does not simplify anything. It hides the flow across five places and adds shared state, which is the trade 9.1 section 6 warns about: visible complexity became hidden coupling, and the metric improved while the code got worse.
5. When simple is the wrong choice
KISS has real limits, and knowing them keeps you from being the person who rejects necessary machinery.
When the problem is genuinely complex. Tax law is complicated. A payment system really does need retries, idempotency, partial refunds and reconciliation. Making that code look simple by leaving parts out does not simplify anything — it moves the complexity into production, where it shows up as wrong money. This is the essential-versus-accidental split from 9.1 section 5: refuse accidental complexity, accept essential complexity and organise it well.
When a proven algorithm beats a naive one. If you have measured that a linear scan is too slow for a million items, the right answer is the indexed structure, not the readable loop. The rule is measure first, then take the fast version and explain it in a comment.
When correctness demands care. Concurrency, money, security and time zones are areas where the obvious code is often subtly wrong. Here "simple" means fewer moving parts and fewer states, not fewer lines and not less rigour.
The honest framing: KISS says do not add complexity that the problem did not require. It never says pretend the problem is smaller than it is.
6. How this comes up in interviews
Interviewers rarely ask "what is KISS". They test it by watching what you build. The signals they read:
You start with the simplest thing that works and say so. "I will start with a single table and a linear scan. At the volumes we estimated that is fine, and if it stops being fine, here is what I would change." That sentence demonstrates judgment. Opening with a sharded, cached, event-sourced design for a problem that does not need it demonstrates the opposite.
You can name what you deliberately left out. "No caching yet — at these numbers the database handles it, and a cache adds an invalidation problem I would rather not own until I need to."
You do not defend cleverness. If asked "could this be simpler?", the strong answer explores it honestly rather than justifying the first draft.
The one-line version if you are asked directly: "Keep the accidental complexity out. The problem's own difficulty stays, and I organise it; anything I added on top of that, I remove. And simple is not the same as short — I would rather read six clear lines than one clever one."
Recall
- Simple means one strand, not braided. It is a property of the code and you can point at the strands. Easy means familiar, which is a property of you. Clever code is often easy for its author and never simple.
- In review, say "the business rule and the scoring mechanism are tangled here", not "this is too complicated". The first is a fact, the second is an opinion.
- Complexity is paid at reading, changing and onboarding time, all of which happen far more often than writing. And your own cleverness expires — the person who cannot read it in six months is usually you.
- Six moves: flatten with early returns, name the intermediate step, one level of detail per function, replace tricks with the obvious version, delete cases that never happen, choose the boring data structure.
- Simple is not short. One dense line with single-letter names is short and complex; six clear lines are longer and simple. The goal is fewer things the reader must work out, not fewer lines. Splitting one clear function into five that share state hides the flow and adds coupling.
- Limits: a genuinely complex problem needs its complexity organised, not hidden; a measured performance need justifies a harder algorithm; money, concurrency, security and time zones need care rather than brevity.
Self-test: Give the difference between simple and easy, with an example of code that is one and not the other. Why is "this is too complicated" a weak review comment, and what would you say instead? Name four of the six simplifying moves. Why is one clever line often worse than six plain ones? When is KISS the wrong advice?