Skip to main content

YAGNI - You Aren't Gonna Need It

Build the thing the current requirement asks for, not the thing you can imagine a future requirement asking for.

A PaymentProcessor interface makes sense the day you add a second payment provider - not the day you write the first one, on the theory that surely there will be a second one eventually.

Why it exists

The cost of guessing wrong is not just the extra code sitting unused. It's that the guessed abstraction gets in the way of the design you would have picked if you had waited for the real second requirement to show you its actual shape - which is almost never the shape you imagined. An interface built for an imagined PayPalProcessor frequently has to be reshaped, not just implemented, the day a real second provider with real constraints (different retry semantics, a webhook instead of a synchronous call) actually arrives.

A guessed abstraction, and the requirement that finally justifies it

// One payment provider exists. This code is built for a second one
// that has never been requested, sized, or even discussed.
interface PaymentProcessor is
method charge(amount, card)
method refund(transactionId)
method getSupportedCurrencies()
method getMaxRetries()
 
class StripeProcessor implements PaymentProcessor is
method charge(amount, card) is
return stripeClient.charge(amount, card)
method refund(transactionId) is
return stripeClient.refund(transactionId)
method getSupportedCurrencies() is
return ["USD"] // guessed - nobody asked for multi-currency yet
method getMaxRetries() is
return 3 // guessed - no retry policy has ever been discussed
 
class PaymentProcessorFactory is
method create(providerName) is
if providerName == "stripe" then
return new StripeProcessor()
throw new UnsupportedProviderError()
// A factory that has only ever produced one thing.

Try it yourself: the business just told you PayPal support ships next quarter, with a real ticket and a real deadline. Sketch what you would extract now that you have a second real case to design against, and check whether it matches the guessed interface above - chances are getMaxRetries() was never the actual seam.

The cost of overapplying it

YAGNI cuts the other way too: it's not a license to ignore a requirement someone actually gave you. If the interviewer or the spec says "we'll add hourly and monthly billing next sprint," that is a real, stated second case - designing only for the one in front of you and ignoring the stated one is under-designing, not discipline. YAGNI defers work for requirements that have not arrived; it does not defer work for requirements that have already arrived and are simply scheduled for later. The same restraint applies to defensive code: skipping validation the interviewer told you is guaranteed to hold is YAGNI; skipping validation on input that genuinely might be malformed is a bug wearing YAGNI's name.

How it relates to its neighbours

YAGNI and Encapsulate What Varies sit on the same axis, pointed in opposite directions. Encapsulate What Varies says: the moment a part of your class changes on its own schedule, wall it off. YAGNI says: not yet, if that schedule is still hypothetical. Both principles agree on the same trigger - a real, current axis of variation - they just disagree about whether an imagined future axis counts. It doesn't, for either of them.

Where you'll see it in the pattern catalog

Factory Method, Abstract Factory, and Strategy are the three patterns most often built a requirement early, "because we'll need it eventually." Every one of them earns its cost the day a second concrete case exists to design against - Factory Method the day a second product type shows up, Strategy the day a second algorithm does. Before that day, the plain, direct version above is not a shortcut; it's the version YAGNI actually recommends.

Check yourself

Question 1 of 3

You write a PaymentProcessor interface on day one, before a second payment provider exists, "because we will need it eventually." Which principle does this violate?