Simple Design

Concept

A design principle of building structure only as far as present needs require, and extracting more structure later, when it is actually needed.

Why it exists

Being good at design was long tied to preparing for the future. Requirements will change, so opening up the places where they might change in advance counted as skill.

The trouble is that the prediction rarely lands. Building payments, Theo added a layer so that several payment methods could be attached later. Two years on, the payment method is still just the one card. Everybody who read that code had to pass through a layer nothing used, and a new hire spent two days working out why the structure was there.

Worse cases exist. A second payment method does arrive, and the pre-built layer doesn't match its shape. What adding recurring billing actually needed was structure for cycles and invoicing, not for swapping payment methods. The pre-built version couldn't be used and couldn't be removed either, so it stayed, now costing twice: once to read past and once to explain.

One judgment comes out of that. Rather than paying now for a prediction likely to be wrong, build only as far as what you currently know and keep it cheap to change. Simple design turns that judgment into a working rule with criteria you can apply in a review rather than an attitude you either have or don't.

How it works

Four criteria decide whether something is simple, and the order they're written in is the priority order. Earlier criteria outrank later ones.

  1. All tests pass. Code that doesn't work isn't simple, however short it is. This comes first because the other three demand changing code. Without tests, changing becomes frightening, and fright sets structure like concrete.
  2. Intent is visible. A reader has to be able to tell what the code is trying to do. A name that doesn't match its behavior, or a rule buried three conditionals deep, fails here.
  3. No duplication. The same rule written in two places produces the situation where only one gets fixed. Note that looking alike and being the same rule are different things — merging coincidentally similar code makes things worse when only one side later changes.
  4. Fewest elements. The minimum number of classes, functions, and layers consistent with the three above. Its position last matters: cutting the count by obscuring intent or leaving duplication is going backward.

The practice consists of two motions: building only what is needed now, and extracting structure the moment it becomes needed. The second is refactoring, changing structure while leaving behavior alone. On the day payment methods genuinely become two, extracting what's common while looking at both real shapes yields something that fits better than the imagined layer did.

Both motions lean on tests, which is why this sits alongside test-driven development. Write the tests first and a structural change is immediately confirmed to preserve behavior, which is what makes changing structure often possible. Moving the design moment later only works when it can be undone. A team without tests that adopts the first half of this principle and skips the second ends up with neither simplicity nor structure.

Where it shows up

It fits where requirements change often: an early product correcting direction from user response, a team whose priorities shift every few weeks. The lower the odds a prediction holds, the higher the odds pre-built structure gets thrown away, and the more the effort spent on it reads as pure loss.

Conversely, some things have to be decided up front — the ones where changing later costs far more than deciding now.

  1. A publicly exposed API. Once callers are attached, changing the shape means changing their code too.
  2. Data structures. Migrating data that already exists comes attached. Alter the columns after a million rows are in and you have a migration and a window of downtime.
  3. Security boundaries. Where permission gets checked is hard to insert afterward, and inserting it late leaves paths where the check is missing.
  4. Irreversible external commitments. Response formats or file specifications written into a contract belong here, where changing your mind is a renegotiation.

What separates these four is asymmetry in the cost of reversal. Structure inside code can be fixed the day after you learn it was wrong; a data structure takes two weeks. So the same team can run simple design on code while drawing data structures in advance, and there is nothing inconsistent about that.

Claiming the principle reduces technical debt is harder to defend. Technical debt is accumulated cost from cleanup postponed in order to move fast. Not building unused structure blocks one kind of debt, but skipping the refactoring at the moment it becomes needed accumulates the other kind.

How it sounds at work

"It's only card right now, so let's skip the interface and use it directly. We'll extract when there are two."

Don't build an abstraction on a prediction; wait for the real second case.

"This abstraction layer has exactly one caller. I think we can remove it."

A proposal to clear out structure nothing uses.

"The data schema isn't a simple-design target. Let's settle this one up front."

Places with a high reversal cost get handled differently.

At Pagedeck the principle comes up most in code review. Theo has one question he asks often: how many places use this structure right now? At one place, no layer gets built. The question is easier to answer than an argument about whether the abstraction is warranted, which is most of why it works.

Noa found the approach uncomfortable at one point. A screen component looked likely to be reused later, and being told to extract it at that point each time felt like agreeing to write the same code twice. So a rule got set. Extract on the second time the same thing is needed, and not before. With a second case in hand you can see what is genuinely shared and what isn't, and what gets extracted tends to last.

What people get wrong

The most common reading is that it means not designing. Not building ahead sounds like not thinking. But three of the four criteria concern structure, and holding to them requires continuous judgment. Telling duplication apart from coincidental resemblance, and choosing names that carry intent, are judgments made more often than stacking layers in advance. What moves is the moment of design, not the total amount of it.

Equating short code with simple code is also frequent. Element count is one of the criteria, so brevity looks like the winner. But that criterion is the fourth, applied after the first three hold. Code compressed into one line by cramming conditions together doesn't show its intent, so it already failed the second criterion. Simple here is much closer to readable, and readable and short frequently diverge. Adding a well-named variable lengthens the code and simplifies it at the same time.

Where to go next

Last updated 2026-09-16