In code reviews I keep running into this comment: // another provider might show up later. Right below it sits an interface with a single implementation, a factory that builds it, and a config key nobody ever reads. There is no second provider on the roadmap — only the possibility of one.

People assume this code was free to write. It wasn’t. Every bit of flexibility added “in case we need it later” is a debt that pays interest every day until the day it’s actually used.

What speculative generality is

Speculative generality is building flexibility today for a requirement that doesn’t exist today. A strategy pattern where one line would do, a generic parameter where one type would do, an event system for a single call.

The justification always arrives in the same words: “later”, “just in case”, “let’s keep it extensible”. Each one is a prediction about the future. And prediction is the most expensive thing in a codebase.

The line items on the bill

Unused flexibility doesn’t sit idle; it pays these line items every day:

  • Reading cost. Everyone who reads the code looks for “is there a second use of this?”, fails to find one, and moves on confused. Flexibility makes you keep thinking about a scenario that doesn’t exist.
  • Maintenance cost. The abstraction has to be updated alongside the real code beneath it on every touch. Even an empty layer demands maintenance.
  • Bug surface. Code that isn’t running can still break code that is — a wrong branch, a bug forgotten in a dead branch, a path that was never tested.
  • Lock-in to the wrong abstraction. This is the most expensive one. The interface you draw from a guess today almost never fits tomorrow when the real need arrives — but by then ripping it out is harder than writing it from scratch.
  • Test burden. Every extra layer of indirection lengthens the test setup and increases the number of mocks.

The “complexity budget” from the boring architecture piece applies here too: speculative generality spends that budget while delivering nothing.

The prediction is almost always wrong

The bitterest part of “just-in-case” code is this: when you actually do need something later, the thing you need isn’t the thing you predicted.

The second payment provider eventually arrives — but it arrives with an async webhook flow your interface never assumed. The second tenant arrives — but with a separate schema, not row-level isolation. Flexibility drawn ahead of time turns out to be an obstacle rather than a help when it meets the real requirement: you have to remove it first.

The moment you know the domain least is the project’s first week; that’s exactly when you make your worst predictions about the future. In the modular monolith piece I argued that boundaries drawn in the first week are almost certainly wrong — the same holds for every early abstraction.

Applying YAGNI in practice

“You Aren’t Gonna Need It” isn’t a license for laziness, it’s a timing rule: add flexibility when the need is proven — not when it’s predicted.

In practice:

  • Write for today’s known case. If there’s one provider, write the one provider. Flat, direct, readable.
  • The rule of three. Wait for three real uses of a pattern before abstracting it. Two might be coincidence; three is a pattern.
  • Open the seam later. When the second provider actually arrives, extracting the interface is an IDE refactor — a few hours. There’s no return on paying those few hours today.

The asymmetry is here: flattening flat code into an abstraction later is easy. Flattening a wrong abstraction later is hard. That asymmetry favors YAGNI.

When is upfront flexibility justified?

It isn’t about deferring everything to the last moment either. The distinction is whether the decision is reversible:

  • Reversible decisions (two-way door). The internal structure of a class, a function’s signature, code organization. Changing these later is cheap — no need to keep them flexible today.
  • Hard-to-reverse decisions (one-way door). The database schema, an API contract you’ve exposed, a data format, a security boundary. Here, thinking ahead isn’t speculation but responsibility — because the cost of getting it wrong is a data migration in production.

The rule: be generously simple inside the code; be cautious where you expose to the outside and write to data. Save flexibility for where undoing it is genuinely expensive.


“We’ll need it later” expresses a fear, not a requirement. Grow code with evidence, not fear.

The easiest code to delete is the code that was never written.