You aren't gonna need it (YAGNI)
Open in the interactive app: practice, quiz, mock interview →In one minute
- YAGNI ("You Aren't Gonna Need It") says: build what today's requirements need, and add the rest when a real requirement arrives. It came out of Extreme Programming in the late 1990s.
- It is a bet about economics. Speculative code costs time to build, costs effort to carry every month, and is often wrong when the real need shows up. Deferring costs only the price of building it later.
- YAGNI is not "skip design". It depends on code that is cheap to change: tests, small modules and the habit of refactoring. Without those, deferral really is expensive.
- It does not cover one-way doors: on-disk and wire formats, public APIs, data you cannot recreate later, and security. Decide those carefully up front.
- Agents are speculation machines. Ask one for a payment client and you may get a provider registry, a retry policy hierarchy and six config knobs. Reviewing for YAGNI is now a daily job.
How it works
The speculative version
A team builds a notification service. The only requirement is email for password resets. The first design review produces this:
class Channel(ABC):
@abstractmethod
def send(self, msg: Message, opts: ChannelOptions) -> DeliveryResult: ...
class ChannelRegistry:
def register(self, name, factory, priority=0, fallback=None): ...
class EmailChannel(Channel): ...
# SmsChannel, PushChannel: "coming soon"There is also a ChannelOptions with 11 fields, a priority-and-fallback system, and a template engine with plugin support. Nine months later SMS arrives. It needs per-country sender IDs and opt-out handling, and the shared send signature fits neither. The team spends two weeks bending the abstraction, and the fallback system has still never run in production.
The YAGNI version
def send_password_reset(email: str, link: str, mailer: Mailer) -> None:
mailer.send(to=email, subject="Reset your password", body=render_reset(link))When SMS arrives, the team has two real channels to compare, so the shared shape is visible instead of guessed. The extraction takes a day, with tests that already cover the email path.
Four costs of a speculative feature
For any capability you are tempted to build early, compare:
- Cost of building it now: the engineering days, plus the delay to whatever ships today.
- Cost of carrying it: every reader must understand it, every refactor must preserve it, every dependency upgrade must keep it compiling, every test run executes its tests.
- Cost of being wrong: when the need arrives it rarely matches the guess. A wrong abstraction costs more than none, because people bend new code to fit it.
- Cost of building it later: often higher than building it now (the code has grown), but you pay it only if the need is real, and you build the right thing.
Building early wins only when the later cost is much higher, the probability of need is high, and the wait before it is used is short. That combination is rarer than it feels in a design meeting.
What YAGNI does not cover
Some decisions are expensive or impossible to reverse:
- Data you cannot recreate. If compliance may later ask who changed each price, an audit log started next year cannot tell you about this year.
- Formats other people depend on. A wire protocol or file format without a version field is costly to evolve. Adding a version byte now is cheap insurance, not speculation.
- Public APIs. Removing a parameter breaks callers; adding one usually does not. Start small.
- Security and correctness. Input validation and authorisation are part of today's requirements, not future features.
The test is reversibility. If adding it later costs about the same as adding it now, wait.
Reviewing agent-written code
Coding agents add generality because it looks like quality: an interface with one implementation, a strategy parameter nobody passes, configuration for values that never change, **kwargs passed through "for flexibility". In review, ask for each: which current caller needs this? If none, remove it. Ask agents to "implement only what the tests require", and treat diffs that add abstractions without adding behaviour as a smell.
The same shape elsewhere
YAGNI is a speculation problem: do work early on a prediction, or wait for the fact. A CPU does speculative execution only because branch predictors are right about 95 percent of the time and a miss costs only about 15 to 20 cycles. Manufacturing calls the opposite of over-building "just in time" inventory: stock costs money to hold, and demand forecasts are often wrong. Capacity planning faces the same trade, which is why autoscaling usually beats provisioning for a 100x peak that may never come.
Down the stack
Layer 1The contract
YAGNI promises that every piece of code has a current reason to exist: a caller, a test from a real requirement, or a deliberate one-way-door decision written down. In return the team promises to keep the code cheap to change, so that adding a capability later costs a refactor, not a rewrite. If that second promise breaks, YAGNI stops paying.
Layer 2The mechanism
What makes deferral safe is tooling, not willpower:
- Tests at the behaviour level let you extract an interface or split a module later without fear.
- Refactoring tools (IDE rename, extract interface,
goplsrename, the TypeScript language service) make "add the seam when the second case arrives" a minutes-long change. - Feature flags let half-built work ship dark, so you build in small slices against real demand.
- Expand-and-contract migrations (add a nullable column, backfill, switch reads, drop the old column) make schema changes incremental, which removes the urge to design every column up front.
- Dead code detection keeps carrying costs visible: Go's
deadcodetool andstaticcheckreport unused functions,tscflags unused locals withnoUnusedLocals, andvulturefinds unused Python code.
Layer 3The machine
Speculative generality has a runtime price. A direct Go method call can be inlined to a few instructions. Wrap the same work in an interface "in case of a second implementation" and the call goes through an itab lookup and an indirect branch, roughly 1 to 3 ns, and usually cannot be inlined. Worse, a value passed through an interface often escapes to the heap, adding an allocation (tens of nanoseconds) and garbage collector work per call.
In JavaScript, V8 optimises a call site that always sees one object shape. A plugin system that routes many shapes through one handle(event) makes that call site megamorphic, and V8 falls back to a slower generic lookup.
Unused dependencies cost elsewhere: a larger bundle means a slower cold start for a serverless function, more transitive packages to patch when a CVE lands, and longer CI builds on every commit. Configuration knobs that are read on every request are hash map lookups with a branch that the code paths behind them never exercise in tests.
Why it's built this way
- Requirements are uncertain, so a large share of features built ahead of need are never used or are used differently than guessed.
- A wrong guess is worse than no guess, because existing abstractions pull new code toward their shape.
- Carrying costs are paid on every change, so they compound with the size and age of the codebase.
- Tests and refactoring tools make the later change cheap, which turns waiting from a risk into the default.
- Some changes cannot be made cheap (lost data, published formats), so those are the exceptions to decide up front.
Diagram
flowchart TD
A["Idea: we might need X"] --> B{"Current caller or requirement?"}
B -- "yes" --> BUILD["Build it now"]
B -- "no" --> C{"Expensive or impossible to add later?"}
C -- "yes: data, formats, public API" --> D["Build the minimal hook now"]
C -- "no" --> E{"Code cheap to change?"}
E -- "yes" --> WAIT["Defer; note the trigger"]
E -- "no" --> F["Invest in tests and refactoring first"]
F --> WAITWorked example
A B2B analytics startup with 6 engineers debated multi-tenant sharding before they had a second large customer. The proposal: 15 engineer-days to build per-tenant database routing now. Estimated cost to add it later, once the codebase was larger: 25 engineer-days. Probability the sales pipeline would actually need it within a year: about 30 percent, and not before month 6. Carrying cost of the routing layer: about 1 engineer-day per month (tests, migrations that must run on every shard, onboarding).
Expected cost of building now: 15 days plus carrying it, about 6 months if needed and 12 months if not, which comes to 15 + (0.3 × 6 + 0.7 × 12) = 25.2 days. Expected cost of waiting: 0.3 × 25 = 7.5 days. They waited, and spent the 15 days on the reporting feature their first customer was asking for.
The same team built the audit log immediately. Building it now cost 3 days. Building it later was effectively 20 days plus permanent loss of history that a regulated customer's contract required, with a 90 percent chance of being asked within 3 months. Expected cost now: 3 days. Expected cost later: 18 days. YAGNI said build it, because the economics, not the slogan, decide.
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| Build only for current needs | Least code, right abstractions from real cases | Some later changes cost more | Default, with good tests |
| Build a minimal hook now | Cheap insurance against a costly retrofit | Small carrying cost | Versions, IDs, audit data, extension points in public APIs |
| Build the full feature early | Ready when demand arrives | Carrying cost, often wrong shape | Need is near-certain and imminent |
| Defer without tests | Fast today | Later change is a rewrite | Never; fix the tests first |
Pitfalls in production
- Using YAGNI to skip design for one-way doors, such as a message format with no version field. Fix: ask "can we add this later for about the same price?" and decide up front when the answer is no.
- Abstractions with one implementation that nobody remembers adding. Fix: inline them, and add the interface back when the second real implementation appears.
- Configuration that has never been changed in production. Fix: audit config values against deploy history and turn unchanged knobs back into constants.
- "Temporary" code with no trigger to revisit it. Fix: record the trigger (the second tenant, 10,000 QPS) next to the deferral so the decision is not lost.
- Deferring with no safety net, so the later change is frightening and keeps getting postponed. Fix: invest in tests at the behaviour level first.
- Agents expanding scope in every diff. Fix: state "only what the failing tests need" in the task and reject unrequested abstractions in review.
Leadership lens
- In design review, ask for every component: which current requirement needs it, and what would it cost to add in six months? Speculation should come with numbers.
- Separate one-way doors from two-way doors explicitly in design documents, and spend review time on the one-way ones.
- Carrying costs are invisible on roadmaps. Track the size of unused flags, configs and abstractions, and budget time to delete them.
- YAGNI only works on teams that are allowed to refactor. If refactoring needs a ticket and a quarter, people will build everything up front, and they will be rational to do so.
Check yourself
Why does YAGNI depend on having good tests?
Show answer
Deferring is only cheap if the later change is cheap. Without tests, adding the capability later means a risky rewrite, so the economics flip toward building early.
A colleague says YAGNI means you should not add a version field to a new event schema. What is wrong with that?
Show answer
A format other systems depend on is a one-way door; retrofitting versioning across producers and consumers is far costlier than one field now. YAGNI applies to features, not to keeping the system changeable.
Why can a wrong early abstraction cost more than no abstraction?
Show answer
Later code is bent to fit the existing shape, so the cost is the original build plus the workarounds plus eventually undoing it.
A feature is certain to be needed, but only in 10 months. Why might deferral still be right?
Show answer
Carrying cost accrues every month until it is used, and the details of a requirement 10 months away often change; if those costs exceed the extra cost of building later, waiting wins.
Watch
- The YAGNI Principle (Venkat Subramaniam, via Dev Tools Made Simple): a short talk on why postponing speculative work keeps a design easier to change.
- Do you really need that abstraction or generic code? (CodeOpinion): a practical look at generic code and abstractions added before there is a need.
Free resources
- Yagni (Martin Fowler): the clearest breakdown of the costs of presumptive features, and why YAGNI depends on refactoring.
- You Arent Gonna Need It (C2 wiki): the original Extreme Programming discussion, with arguments on both sides.
- The Wrong Abstraction (Sandi Metz): why duplication is cheaper than an abstraction built on a guess.
- Is Design Dead? (Martin Fowler): how evolutionary design and YAGNI fit together, and where up-front design still matters.
- Beck Design Rules (Martin Fowler): "fewest elements" as one of Kent Beck's four rules of simple design.