Speculative Generality: Plumbing for a Swimming Pool You May Never Build
Learn the Speculative Generality smell with a house-building story. Understand YAGNI, why guessing future needs backfires, and how to collapse unused abstractions.
🏊 The house with plumbing for an imaginary swimming pool
Mr. Verma is building a house in Indore. He has saved for it his whole life — government job, careful man, two sons. The architect, a patient young woman named Sneha, spreads the drawings on the table and asks: "Two floors, correct?"
"Build for four," says Mr. Verma proudly. "Someday my sons will live above us, with their families. Make the foundation for four floors. And put plumbing on the terrace — someday we may build a swimming pool, the children would love it. Also lay extra electrical conduits in every wall — someday we may want this home automation everyone is talking about. And make the kitchen wall removable — someday we may extend the kitchen."
Sneha pauses. "Sir, each 'someday' costs money today. Should we—"
"Build for the future!" says Mr. Verma. "That is wisdom."
So Sneha obeys. The foundation is built four floors strong: extra steel, extra cement, extra months of work. Pipes for the imaginary pool run through the walls of real bedrooms. Conduits for imaginary automation snake under every floor tile. The removable kitchen wall needs a special steel beam, ordered from another state.
The bill comes: nearly double the cost of a simple two-floor house. The family's savings are gone, so the interiors stay half-finished for years. Mrs. Verma cooks in a kitchen with bare plaster walls — walls that are, at least, theoretically removable.
And the future? The sons get jobs in Bengaluru and Canada. No third floor ever comes. The pool? Twenty years later, the terrace pipes have rusted shut — and when Mrs. Verma wants a terrace garden instead, the plumber shakes his head: "First we must rip out all these pool pipes, madam. They are exactly in the wrong place." The removable kitchen wall develops a crack, because special beams need special maintenance that nobody remembered to do.
Every guess about the future cost real money on day one, got in the way every day after, and had to be demolished when the real future finally arrived in a different shape.
In code, this is Speculative Generality: machinery built for a guessed future, paid for in real present-day complexity.
🤔 What is this smell?
Speculative Generality is extra structure — interfaces, abstract base classes, hook methods, parameters, generic types, factories, plugin systems — created not for any present need, but for a future need that someone guessed. The guess rarely comes true in the imagined shape, but the structure stays, and every reader and every change pays its price.
This smell has a famous counter-principle: YAGNI — "You Aren't Gonna Need It" — a battle cry from Extreme Programming. Martin Fowler's bliki essay on Yagni is the definitive treatment, and it gives the costs precise names:
- Cost of building — the time spent building the presumed feature is taken from features needed now.
- Cost of delay — the real, valuable feature ships later because effort went to the guess.
- Cost of carry — the speculative structure makes the codebase heavier; every future change must step around it.
- Cost of repair — when the real need arrives in a different shape (it almost always does), the wrong abstraction must be dismantled first.
Fowler's sharpest point: even if the guessed feature is needed someday, building it early is usually still wrong — you carry its weight the whole time, and you built it with the least information you will ever have.
The honest test for any abstraction: "Do I have two real cases, or one real case and one imagined one?" Two real cases earn an abstraction — they show you its true shape. One real case plus a hunch earns only simple, concrete code. Let the second case, when it actually arrives, tell you what the abstraction should look like.
College corner: There is a clean economics framing for YAGNI. Building flexibility early is like buying an option — you pay a premium now for the right to benefit later. The problem is that software options are usually mispriced: the premium (build + carry cost) is paid with certainty, while the payoff depends on a low-probability guess about both the need and its shape. Lean software development calls the alternative deferring decisions to the last responsible moment — the point where waiting longer would close off options. Combine that with Kent Beck's observation that well-factored simple code keeps the cost of change curve flat, and you get the formal version of YAGNI: when refactoring is cheap, the option to abstract later is nearly free, so paying a certain premium today for an uncertain payoff is simply a bad trade.
Here is the whole territory in one map:
🔍 How to spot it
The checklist — count how many your project ticks:
- An
interfaceor abstract base class with exactly one implementation, and no second one in sight after months or years. - Hook methods that no subclass ever overrides.
- Parameters, options, or "strategy" slots that every caller passes the same value to.
- Generic type parameters only ever instantiated with one concrete type.
- Layers named "Manager", "Handler", "Provider", "Factory" that exist to support extensions never written.
- Plugin systems with zero plugins; callback registries with zero callbacks; event buses with one publisher and one subscriber sitting in the same module.
- Code whose only justification, when you ask, is: "someone might need this later."
| Sign | The question to ask | Speculative if... |
|---|---|---|
| Interface + 1 implementation | Is there a real second implementation or test seam today? | No, "maybe later" |
| Config option | Does any environment set a different value? | All set the same value |
| Hook method | Does any subclass override it? | None ever has |
Generic <T> | Is it used with more than one type? | Only ever <Invoice> |
| Factory | Can it ever return a different product? | Always returns the same class |
| "Extensible" plugin API | How many plugins exist? | Zero, forever |
And when teams honestly review their old "future-proofing" decisions, the scoreboard usually looks like this:
Roughly one guess in ten pays off as imagined. Those are the odds you are buying with every speculative layer.
⚠️ Why it is a problem
Cost 1: Indirection with no payoff. To understand one simple operation, the reader now hops: interface → factory → abstract base → the single concrete class — and discovers at the end that the journey explained nothing, because there was only ever one case. Four files to learn what one function would have said.
Watch a new teammate make exactly this journey:
Cost 2: A false promise in the design. An abstraction is a signpost saying "variation lives here!" The reader trusts it and goes hunting for the other implementations... which do not exist. Speculative structure makes the design lie, and readers waste time on the lie repeatedly.
Cost 3: The guess is almost always the wrong shape. When the real second case finally arrives, it never fits the slot built for it. The pool pipes are exactly where the garden needs soil. Now you do double demolition work: remove the wrong abstraction, then build the right one. Plain concrete code would have been the cheaper starting point.
Cost 4: It breeds the other Dispensables. The empty classes left behind become Lazy Classes. The hooks nothing calls become Dead Code. One speculative module can produce a whole family of smells.
The carry cost is easy to underestimate because it is paid in small coins, every day, by everyone:
And here is the life story of a typical speculative abstraction — note how rarely the happy transition fires:
Follow the two paths to the end. The speculative path loses in every branch — even when the guess comes true, it usually comes true in the wrong shape. The YAGNI path never loses: at worst you refactor when the second case arrives, with full information in hand.
💻 A real-life code example
Mr. Verma's house, as a billing feature. The requirement was one line: "Give 10% discount on festival days." Here is what got built instead:
// Smelly version: a framework for a one-line requirement
interface DiscountStrategy {
apply(amount: number): number;
}
interface DiscountStrategyProvider {
provide(): DiscountStrategy;
}
// the only strategy that has ever existed
class FestivalDiscount implements DiscountStrategy {
apply(amount: number): number {
return amount * 0.9;
}
}
// the only provider that has ever existed
class DefaultDiscountStrategyProvider implements DiscountStrategyProvider {
provide(): DiscountStrategy {
return new FestivalDiscount(); // always. only. forever.
}
}
class DiscountEngine<TContext> { // TContext: only ever 'Order'
constructor(
private readonly provider: DiscountStrategyProvider,
private readonly roundingMode: string = "standard", // every caller: "standard"
) {}
// hook for subclasses... that were never written
protected beforeApply(_context: TContext): void {}
run(context: TContext, amount: number): number {
this.beforeApply(context);
return this.provider.provide().apply(amount);
}
}
// caller — four floors of foundation for a one-floor house:
const engine = new DiscountEngine<Order>(new DefaultDiscountStrategyProvider());
const price = engine.run(order, amount);The audit:
- Two interfaces, one implementation each. Pool plumbing.
- A provider that always provides the same thing. A removable wall that was never removed.
roundingMode— a parameter every caller passes the same value to. Conduits with no wires.TContext— a generic used with exactly one type. Foundation for floors never built.beforeApply— a hook no subclass overrides. A doorway bricked up since construction.
What does all this machinery actually do? Multiply by 0.9. One line of business logic, wearing five layers of armour against an enemy that never came. The blueprint of the construction site:
🧹 Cleaning it up, step by step
Demolition day. Work from the outside in.
Step 1: Remove the parameter nobody varies. Every caller passes roundingMode = "standard". Apply Remove Parameter and use the standard behavior directly.
Step 2: Inline the hook nobody overrides. beforeApply does nothing and no subclass changes that. Apply Inline Method — it disappears without a trace.
Step 3: Fold the provider and the engine. DefaultDiscountStrategyProvider always returns FestivalDiscount; the engine only forwards to it. Apply Inline Class twice — the factory folds into its caller, the wrapper folds away.
Step 4: Collapse the one-implementation interfaces. With only FestivalDiscount left under DiscountStrategy, apply Collapse Hierarchy: keep the concrete thing, delete the abstract promise.
Step 5: Look at what remains.
// Clean version: the requirement, stated directly
const FESTIVAL_DISCOUNT_RATE = 0.1;
function festivalPrice(amount: number): number {
return amount * (1 - FESTIVAL_DISCOUNT_RATE);
}
// caller:
const price = festivalPrice(amount);Twenty-five lines of framework became three lines of truth. A new teammate understands it in five seconds. And here is the beautiful part: this simple version is the best possible starting point for the real future. If next year marketing genuinely adds a "loyalty discount" and a "clearance discount", you will have two or three real cases — and then extracting a strategy interface takes twenty minutes and produces the right shape, designed from facts instead of fantasies.
🟦 The same smell in C#
A report exporter "ready" for formats that never came:
// Before: an export framework with exactly one export
public interface IReportExporter
{
byte[] Export(Report report);
}
public abstract class ReportExporterBase : IReportExporter
{
public byte[] Export(Report report)
{
OnBeforeExport(report); // never overridden
return DoExport(report);
}
protected virtual void OnBeforeExport(Report report) { }
protected abstract byte[] DoExport(Report report);
}
public class PdfReportExporter : ReportExporterBase // the only child, ever
{
protected override byte[] DoExport(Report report)
=> PdfWriter.Write(report);
}
public static class ReportExporterFactory
{
public static IReportExporter Create(string format = "pdf") // only "pdf" is ever passed
=> new PdfReportExporter();
}Four types and a factory to call one library method. After demolition:
// After: the one real capability, stated plainly
public class ReportExporter
{
public byte[] ExportPdf(Report report) => PdfWriter.Write(report);
}When Excel export becomes a committed, scheduled requirement — not a hallway "we should someday" — that is the moment to introduce the interface, shaped by two real formats.
A Python flavour, where the speculation often hides in **kwargs and base classes:
# Before: a base class and options built for imaginary subclasses
class NotificationSenderBase:
def send(self, message, priority="normal", retry_policy=None, **kwargs):
self.before_send(message) # no subclass overrides this
self._do_send(message)
def before_send(self, message):
pass
# After: the one thing the app actually does
def send_notification(message: str) -> None:
sms_gateway.send(message)Every caller passed priority="normal", nobody supplied a retry_policy, and **kwargs collected dust. Three honest lines replace the imaginary framework.
🏢 Where this smell hides in real projects
- The automatic
IFooServicefor everyFooService. Some codebases pair every class with an interface by reflex. Where no seam is used, each pair is one speculative layer. (Modern DI containers and mocking tools work fine with concrete classes in many cases.) - "We might switch databases someday" abstraction layers. Heavy repository-on-repository wrappers to keep the ORM swappable — for a swap that, in most companies, never happens. Meanwhile every query pays the wrapper tax.
- In-house frameworks around frameworks. A wrapper around the HTTP client, around the logger, around the message bus — "so we can change vendors easily" — each wrapper thinner in features and poorer in documentation than the thing it wraps.
- Config options nobody sets. Dozens of knobs in
appsettingsthat every environment leaves at the default. Each knob is a code path that must be tested and reasoned about. - Premature microservices and plugin architectures. Splitting a small product into services or plugin slots "for scale we will need later" — paying distributed-system complexity today for traffic that may never come.
- Generic utility types with one user.
Result<T, TError, TContext>machinery used by exactly one call site with exactly one shape. - Fields and methods passed to subclasses that never use them. Built so "child classes can use it later" — they never do.
⚖️ When it is okay to ignore
YAGNI is a principle, not a religion. Some up-front generality is earned — the judgment is about evidence versus hunch.
| Situation | Build the abstraction now? | Why |
|---|---|---|
| Published API / library boundary with external users | Yes | Outsiders depend on the contract; changing it later breaks them |
| Second implementation already committed on the roadmap | Yes | Two real cases exist — one is just slightly in the future |
| Test seam needed today to isolate a hard dependency | Yes | That is a present need, not speculation |
| Regulatory or platform boundary you must support | Yes | The requirement is real and external |
| Extension point in a framework shipped to other teams | Yes | The "future users" are real customers of the API |
| "We will surely need multiple payment providers" (one exists) | No | One real case + one imagined one = speculation |
| "Make it generic, someone might reuse it" | No | Wait for the someone; let them shape it |
| "Add the parameter now to avoid touching callers later" | No | Touching callers later is cheaper than carrying the knob forever |
You can plot any proposed abstraction on this chart before building it. Only the top-right corner deserves up-front structure:
One more honest nuance from Fowler's Yagni essay: YAGNI applies to features and flexibility, not to quality. Do not use "YAGNI!" as an excuse to skip tests, ignore naming, or write tangled code — making code easy to change later is the very thing that makes YAGNI safe. You can afford to build only what today needs precisely because clean, well-tested code is cheap to extend tomorrow.
🛠️ Which refactorings cure it
| Speculative structure | Curing refactoring |
|---|---|
| Abstract class / subclass for variation that never came | Collapse Hierarchy |
| Factory, provider, or wrapper with one product | Inline Class |
| Hook or delegating method nobody customizes | Inline Method |
| Parameter every caller passes identically | Remove Parameter |
| Interface with one implementation, no real seam | Remove the interface; use the class directly |
| Unused "future" fields and methods | Delete them — see Dead Code |
📦 Quick revision box
+--------------------------------------------------------------+
| SPECULATIVE GENERALITY — QUICK REVISION |
+--------------------------------------------------------------+
| Story : A 2-floor family building 4-floor foundations |
| and pool plumbing for a pool that never comes. |
| Smell : Interfaces, hooks, factories, parameters, and |
| generics built for GUESSED future needs. |
| Why bad : Pay 4 times — build cost, delay cost, carry |
| cost, and repair cost when the real future |
| arrives in a different shape. |
| YAGNI : "You Aren't Gonna Need It" (XP; Fowler's bliki). |
| Build for today; extract abstractions from REAL |
| cases, not imagined ones. |
| Test : "Two real cases, or one real + one imagined?" |
| Two real -> abstract. One + hunch -> stay simple. |
| Cures : Collapse Hierarchy, Inline Class, Inline Method, |
| Remove Parameter, plain deletion. |
| Caution : YAGNI is about features, never about quality. |
+--------------------------------------------------------------+✏️ Practice exercise
A school-noticeboard app needs to do exactly one thing: show today's notices, newest first. A very enthusiastic developer delivered this:
interface NoticeSource<TFilter> {
fetch(filter: TFilter): Notice[];
}
interface NoticeRanker {
rank(notices: Notice[]): Notice[];
}
class DatabaseNoticeSource implements NoticeSource<DateFilter> {
fetch(filter: DateFilter): Notice[] {
return db.notices.where("date", filter.date);
}
}
class NewestFirstRanker implements NoticeRanker {
rank(notices: Notice[]): Notice[] {
return [...notices].sort((a, b) => b.time - a.time);
}
}
class NoticeBoardEngine<TFilter> {
constructor(
private source: NoticeSource<TFilter>,
private ranker: NoticeRanker,
private maxItems = 100, // every caller: 100
private theme = "default", // every caller: "default"; unused inside
) {}
protected onBeforeRender(): void {} // no subclass exists
display(filter: TFilter): Notice[] {
this.onBeforeRender();
const all = this.source.fetch(filter);
return this.ranker.rank(all).slice(0, this.maxItems);
}
}Your tasks:
- Make a speculation list: identify every piece of machinery that exists for an imagined future. (Count the interfaces, the generic, the unused parameter, the constant-valued parameter, and the hook — at least six items.)
- For each item, name the demolition refactoring you would use (Collapse Hierarchy, Inline Class, Inline Method, Remove Parameter, or plain deletion).
- Write the clean version. Target: one small function or one small class, under ten lines, that fetches today's notices and sorts them newest first.
- Now the plot twist: six months later, the school really does ask for a second source — notices from a parents' WhatsApp group export. Sketch (in three or four lines) the abstraction you would introduce now, shaped by the two real sources. Notice how much better-informed this design is than the original guess.
- Place the original
NoticeBoardEngineand your six-month abstraction on the quadrant chart from Figure 10. Which quadrant does each land in, and why? - Apply the honest test to your own current project: find one interface with a single implementation. Write one sentence judging it — real seam, or pool plumbing?
- Bonus: explain in two sentences why the clean ten-line version was the better starting point for the WhatsApp feature than the original "extensible" engine. (Hint: which version was easier to reshape?)
When your noticeboard does today's job in ten honest lines — and you can explain exactly what evidence would justify each abstraction — you have understood YAGNI the way Fowler meant it, and Sneha the architect would finally get to build a sensible house.
Frequently asked questions
- What is Speculative Generality in simple words?
- It is extra machinery — interfaces, base classes, parameters, hooks, plugin systems — built today for a future need that is only a guess. The guess usually turns out wrong or never arrives, but the extra complexity stays and everyone pays for it on every read and every change.
- What does YAGNI mean?
- YAGNI stands for 'You Aren't Gonna Need It'. It is an Extreme Programming principle, explained in depth on Martin Fowler's bliki: do not build a capability for a presumed future need; build it when the need is real. You save the cost of building, carrying, and usually un-building the wrong guess.
- Why is building for the future a bad idea? Isn't planning good?
- Planning is good; guessing in code is not. Future requirements almost never arrive in the shape you guessed, so the speculative abstraction must first be dismantled before the right one can be built — making the total work larger, not smaller. Concrete, simple code is the easiest starting point for whatever future actually comes.
- How is Speculative Generality different from Lazy Class and Dead Code?
- They are close relatives. Speculative Generality is the act of building unneeded structure; the empty classes it leaves behind are Lazy Classes, and the hooks nothing ever calls are Dead Code. One smell creates the other two.
- When is up-front abstraction actually justified?
- When there is evidence, not a hunch: a published API that external users already depend on, a second implementation that is committed on the roadmap, a real test seam you need today, or a regulatory or platform boundary you must support. The honest test: do I have two real cases, or one real case and one imagined one?
Further reading
Related Lessons
Lazy Class: The Watchman Whose Only Job Is Pressing One Lift Button
Learn the Lazy Class code smell with a society watchman story. Find classes that do too little to deserve existing, and cure them with Inline Class.
Dead Code: Old Furniture Blocking the Storeroom 'Just in Case'
Learn the Dead Code smell with a storeroom full of unused furniture. See why unreachable code costs real money, with the Knight Capital story and easy fixes.
Shotgun Surgery: One Small Change, Ten Offices to Visit
Learn the Shotgun Surgery code smell with an address-change story, simple definitions, TypeScript and C# examples, a clear comparison with Divergent Change, and practice.
Collapse Hierarchy: When Parent and Child Classes Become the Same
Learn the Collapse Hierarchy refactoring with a housing-society committee story, step-by-step merging of a superclass and subclass in TypeScript and C#, and the checks that tell you when a hierarchy has stopped earning its keep.