Appearance
9.3.4 — YAGNI: You Aren't Gonna Need It
The delivery app needs to send an email when an order is placed. An engineer, thinking ahead, builds this:
typescript
interface NotificationChannel { // (1)
send(to: Recipient, message: Message): Promise<void>;
}
interface ChannelSelector { // (2)
choose(user: User, kind: MessageKind): NotificationChannel;
}
interface TemplateEngine { // (3)
render(templateId: string, locale: string, data: object): Message;
}
class NotificationOrchestrator { // (4)
constructor(
private readonly channels: Map<string, NotificationChannel>,
private readonly selector: ChannelSelector,
private readonly templates: TemplateEngine,
private readonly retryPolicy: RetryPolicy,
private readonly rateLimiter: RateLimiter,
) {}
// … 200 lines
}Line (1) is a role so any channel can be plugged in. Line (2) is a strategy for picking between channels. Line (3) is a templating layer with language support. Line (4) wires it together with retries and rate limiting.
Six files, five interfaces, about three hundred lines. What ships is one email, sent through one provider, in English.
Eighteen months later, here is the state of it. SMS was never added, because the company decided text messages were too expensive in its market. Language support was never used, because the app launched in one country. The rate limiter was never configured, so it passes everything through. And when the company did eventually add push notifications, the design did not fit: push messages need a title and a body and a deep link, which Message has no room for, so the whole thing was rewritten anyway.
Every line of that flexibility was paid for. None of it was collected.
1. What the principle says
YAGNI comes from Extreme Programming, and Ron Jeffries' phrasing is blunt:
Always implement things when you actually need them, never when you just foresee that you need them.
The word carrying the weight is foresee. The principle is not "never plan". It is a claim about the accuracy of predictions: you are much worse at guessing future requirements than you believe, and code built for a guess is usually wrong in a way that costs more than having built nothing.
Note what it forbids. Not planning, not thinking ahead, not architecture. It forbids building for a requirement that has not arrived.
2. The four costs, because "it might be useful" sounds free
The argument for speculative work is always the same: it is cheap now and expensive later. That is usually false, and here is why in four parts.
Cost of building. The obvious one. Days spent on the guess are days not spent on what users are waiting for.
Cost of carrying. This is the one people forget, and it is the largest. Every unused abstraction is read by everyone who passes through, appears in every search, must be updated during every refactor, and is discussed in every design conversation. A team pays that tax weekly, forever, for something that does nothing.
Cost of blocking. Wrong flexibility is worse than none, because it looks like a solution. When push notifications finally arrived, the existing Message type was almost right, so the first instinct was to bend it rather than replace it. Two weeks were spent bending it before somebody admitted it did not fit. Had there been no abstraction at all, the right one would have been designed in an afternoon from two real cases.
Cost of being wrong about the shape. This is the deep one. An abstraction designed from one example is shaped like that example. You cannot see which parts are general and which are specific to the case in front of you until you have a second case. So the speculative abstraction is not merely early — it is built from insufficient information, and it will be wrong in ways you cannot detect at the time.
3. What YAGNI does not say
The principle gets misused as a licence for carelessness, so the limits matter as much as the rule.
It does not forbid thinking. Knowing that SMS might arrive one day is useful. It should influence how you name things and where you draw file boundaries, both of which are free. Write notifyCustomer(order) rather than sendOrderEmail(order), and put it in notifications/ rather than in email.ts. You have built nothing speculative and you have made the future change easy.
It does not apply to things that are expensive to add later. Some decisions are genuinely hard to reverse, and for those, the cost of guessing wrong is smaller than the cost of retrofitting:
| Do it now | Because retrofitting is brutal |
|---|---|
| Store money as integer minor units | changing the column type means migrating every row and every report |
| Put a primary key on every table | adding identity to existing rows is a data project |
| Store timestamps in UTC with a time zone | you cannot recover the original zone later |
| Log who did what, from day one | you cannot reconstruct an audit trail after the fact |
| Hash passwords properly | there is no fixing this after a breach |
| Keep secrets out of the code | a leaked key stays leaked |
Read the right-hand column: none of these are features. They are decisions that are cheap today and nearly impossible tomorrow. Martin Fowler's test is the useful one — ask whether this is a reversible decision. Reversible decisions can wait. Irreversible ones deserve thought now.
It does not excuse a mess. "YAGNI" is not a reason to skip error handling, input validation at a trust boundary, or naming things properly. Those are not speculative features. They are part of the code working at all.
It does not mean ignoring known requirements. If the roadmap says SMS ships next quarter and the team has committed to it, that is not a guess. Build for it.
4. Recognising speculative work
Six tells. Any one of them is worth a conversation in review.
An interface with one implementation, and nobody can name the second. The test is to ask out loud. If the answer is "in case we ever…", it is speculation. If it is "the in-memory version we use to run this without a database", that is a real second implementation and the interface is justified today.
Configuration for a value that has never changed. A setting nobody has ever set is a branch you maintain and never exercise. Inline the value.
A parameter every caller passes the same way. Ten call sites all passing true. The parameter is not flexibility, it is a fork nobody uses.
Generic code with one concrete use. Repository<T> with only Repository<User>. The generic machinery makes every reader work harder and buys nothing until the second type arrives.
"Phase 2" comments. A hook for a feature nobody has scheduled. Either it is coming, in which case build it, or it is not, in which case delete the hook.
Framework machinery adopted for a size you do not have. A message queue for four hundred daily orders, a caching layer for a query that takes three milliseconds, a plugin system for two plugins that you wrote yourself and can edit directly.
5. What to do instead
YAGNI is easier to follow when you know the alternative, and the alternative is not "write bad code".
Write the simplest thing that works, and make it easy to find. One clear function in a well-named file is trivial to change later. The changeability comes from the code being small and obvious, not from having plug points.
Spend your effort on naming and placement, which are free. A well-named module boundary is most of the value people think they get from an abstraction, at none of the cost.
Wait for the second case, then abstract from two real examples. The abstraction you extract from two known cases fits both. The one you invent from zero fits neither reliably. This is the rule of three from 9.3.2, and it is the same reasoning.
Keep the irreversible decisions on a short list and think hard about those. Data formats, identifiers, time handling, money representation, audit records, security. That is where planning pays; everywhere else, respond to what arrives.
Write down what you deliberately did not build. A one-line note — "single channel for now; add a role when the second one is real" — tells the next reader this was a decision rather than an oversight, and it stops somebody adding the abstraction anyway out of politeness.
6. YAGNI against Open/Closed
These two look like a direct contradiction, and being able to reconcile them is a senior-level answer worth having ready.
Open/Closed (9.3.6) says: new behaviour should arrive as new code, not as edits to working code. That means plug points.
YAGNI says: a plug point nobody uses is dead weight.
The reconciliation is about when, not whether:
Apply Open/Closed to the axis of change that has already shown itself. Apply YAGNI to every other axis.
Concretely, in the payments example from 9.1: the business has added three payment methods in two years, so "payment method" is a proven axis of change and deserves an interface today. That same system has had one currency since launch, so "currency" is not a proven axis, and building a multi-currency abstraction now would be speculation, however plausible it sounds.
The evidence is in the git history. Look at what has actually changed repeatedly, and make that easy. That is not guessing — it is reading. Everything else stays concrete until it moves.
Recall
- YAGNI: build things when you actually need them, never when you merely foresee needing them. It forbids building for a guess; it does not forbid thinking.
- Four costs of speculative work: building it, carrying it forever (the largest — every reader, every search, every refactor), blocking the real solution when it arrives because the wrong abstraction looks almost right, and being shaped by one example so it does not fit the second.
- Exceptions worth building now, because retrofitting is brutal: money as integer minor units, primary keys, UTC timestamps, audit logging, password hashing, secrets out of the code. The test is whether the decision is reversible.
- Tells of speculation: an interface whose second implementation nobody can name, config never configured, a parameter every caller passes identically, generics with one concrete use, "phase 2" hooks, and infrastructure sized for traffic you do not have.
- Do instead: the simplest thing that works, in a well-named place. Naming and file placement are free and buy most of what people think abstraction buys. Wait for the second real case, then extract from two examples.
- Against Open/Closed: apply Open/Closed to the axis of change that has already proven itself, YAGNI to every other axis. The evidence is in the git history, which makes it reading rather than guessing.
Self-test: Which word in the YAGNI statement carries the meaning, and why? Name the four costs, and say which is largest and why people forget it. Give three things you should build before you need them, and the property they share. How do you reconcile YAGNI with Open/Closed in one sentence?
Quiz Bank
InterviewYAGNI and Open/Closed seem to contradict each other. How do you resolve that?
They look opposed because one says "add plug points so new behaviour does not require edits" and the other says "do not build what you do not need yet". Both are correct, and the resolution is about when, not whether.
The rule: apply Open/Closed to the axis of change that has already proven itself, and YAGNI to every other axis.
A system usually varies along several dimensions, and only some of them actually move. In a payments system, "which payment method" might have changed three times in two years, while "which currency" has never changed since launch. So the payment method deserves an interface today, because the evidence is in front of you and the fifth method is coming. A multi-currency abstraction would be speculation, however reasonable it sounds in a design meeting.
The evidence source matters and is the part that makes this concrete: the git history. Look at which files have been edited repeatedly for the same class of reason. That is not prediction, it is observation, and it turns "will we need this?" into "we have needed this eleven times".
The failure on each side, so the trade is visible. Over-applying Open/Closed gives you plug points nobody plugs into, which every reader must still understand and every refactor must still carry. Over-applying YAGNI gives you the nine-file change every time a payment method is added, plus the silent switch somebody eventually misses. Neither is a small cost, which is why the answer is an axis-by-axis judgement rather than a blanket rule.
One extra move worth mentioning: the cheap half of Open/Closed — naming and file placement — costs nothing and can be done immediately. Calling it notifyCustomer in a notifications/ folder rather than sendOrderEmail in email.ts builds no machinery and makes the eventual abstraction a small change instead of a rename across the codebase.
AppliedA teammate wants to add a plugin system so future teams can extend the checkout. There are no plugins today. What do you say?
Start by taking the goal seriously, because the underlying wish — that other teams can extend checkout without editing it — is a good one, and dismissing it with "YAGNI" wins the argument and loses the colleague.
Then ask three questions. Who is the first plugin, and when does it ship? If nobody can name one, the shape of the plugin interface is a guess, and interfaces designed from zero examples are usually wrong in ways nobody can see yet. What would the plugin need to touch? If the answer is "the cart, the pricing, the payment step and the order record", then this is not a plugin system, it is a request for four separate extension points that each need their own design. And who owns the interface once it exists? A plugin interface is a public contract, so every future change to checkout has to keep it working, which is a permanent constraint the team is signing up for.
Then offer the cheap half now. Naming and boundaries cost nothing: make sure the checkout flow is expressed as clear named steps in a folder of its own, so that when the first real extension arrives the change is small and local. That gives most of the future benefit with none of the carrying cost.
And name the trigger. "When we have two real extension requests, we design the interface from both of them, and it will fit. Today we would be designing from zero." That converts a rejection into a scheduled decision, which is much easier to agree to, and it is honest — the plugin system may well be right in six months.
If they overrule you and it ships anyway, ask for one thing: that the first real extension be built through the plugin interface immediately, by somebody outside the team. An extension point that has never been used from the outside is almost always subtly unusable, and finding that out in week one is far cheaper than in year two.