Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Speculative Generality

Antipattern

A recurring trap that causes harm — learn to recognize and escape it.

Adding hooks, abstractions, parameters, and extension points for future needs that have not become real requirements.

Also known as: Just-in-case design, future-proofing, premature generalization

Understand This First

  • YAGNI — the heuristic speculative generality violates.
  • Abstraction — the tool that becomes harmful when it hides an unreal distinction.
  • Architecture Astronaut — the broader design-level cousin of this code-level smell.

Speculative generality is what happens when a developer or agent says, “we’ll probably need this later,” and then turns that guess into code today. The guessed future gets a parameter, an interface, a base class, a plugin slot, a factory, or a configuration option. Nothing uses it yet. The code carries it anyway.

Symptoms

  • A function accepts a parameter that every caller passes the same way.
  • An interface has exactly one implementation and no scheduled second implementation.
  • A base class exists only so a future subclass can appear someday.
  • The agent adds a plugin system, provider abstraction, or strategy object before any second plugin, provider, or strategy exists.
  • Unit tests exist solely to exercise unused hooks that production code never calls.
  • A reviewer asks what requirement the extension point serves, and the answer is “we might need it later.”
  • Removing the generalized path makes the current feature easier to read and breaks nothing except tests written for the generality itself.

Why It Happens

Speculative generality starts with a reasonable fear. Changing code later can be expensive, so it feels prudent to leave room now. The mistake is converting fear into structure before the real second case arrives. You’re not preserving optionality; you’re committing to one imagined future and asking every future reader to carry it.

Agents are especially prone to this trap. They have seen polished examples where small mechanisms grew into reusable frameworks. When a prompt says “make this extensible” or “build it production-ready,” the model often reaches for the shapes that appear in mature systems: provider interfaces, factories, abstract base classes, feature toggles, and handler registries. Those shapes are not wrong. They are wrong now when the current system has one path through the code.

Human teams do the same thing for social reasons. Generalized code looks thoughtful. A pull request with an abstraction can feel more senior than a direct implementation, and a design doc with “future extensibility” sounds safer than one that says “we don’t know yet.” But speculation dressed as engineering is still speculation.

The deeper cause is discomfort with deletion. It feels wasteful to throw away a clever hook. So the hook survives because someone might need it, even after nobody can name who that someone is. By then the hook has become part of the local convention, and agents will preserve it because it exists.

The Harm

Speculative generality makes code harder to read before it has helped anyone. Every unused path asks the reader to model a situation that is not happening. A future maintainer has to decide whether an unused parameter is dead, reserved, load-bearing, or waiting for a customer that never arrived. That ambiguity slows every change around it.

It also makes the future worse, not better. The future need, when it arrives, rarely matches the guessed one. The provider abstraction imagined for a second payment vendor assumes the wrong error model. The plugin system expects synchronous hooks, but the actual integration needs streaming. The one-interface architecture has already shaped tests, mocks, and dependencies, so the real future now has to work around yesterday’s guess.

In agentic coding, the harm compounds because the agent treats unused generality as instruction. If the first patch adds a one-implementation interface, the next patch extends it. If an unused mode parameter exists, the agent may invent a second mode rather than delete the parameter. A speculative seam becomes a path of least resistance for more speculative code.

The cost is not only lines of code. It is attention. The reviewer spends time checking behavior that cannot occur. The test suite runs through branches no user can reach. Documentation explains options no one should set. The team pays maintenance tax on an imaginary customer.

The Way Out

Delete the future until it has a name. If you cannot name the second caller, second vendor, second data shape, or second deployment, keep the code concrete. Write the simple version and make it easy to change when the second case arrives.

Use four checks:

Name the second case. “Another database someday” is not a case. “We are adding DynamoDB support for the audit-log service next quarter” is. If the second case has no owner, date, or concrete difference from the first, it is a note for later, not code for today.

Prefer a note over a hook. When you notice a plausible future need, write it in the issue, design doc, or code comment if the context genuinely matters. Do not create a public parameter or abstraction whose only job is remembering the idea. Notes are cheap to delete. APIs are promises.

Inline the unused path. If a class, function, parameter, or interface exists only for possible future variation, remove it and collapse the call chain. Fowler and Beck’s refactoring advice is deliberately mundane here: inline the class, inline the function, collapse the hierarchy, remove the dead code.

Test the real behavior. Delete tests that only prove unused machinery exists. Keep tests that protect current behavior and future-facing invariants you actually need today, such as data compatibility, authorization boundaries, or migration safety.

When working with an agent, constrain the altitude directly: “Build only the current path. Don’t add an abstraction, mode flag, provider interface, plugin hook, or strategy object unless there are two concrete uses in this change. If you think one is justified, name both uses before writing code.”

Tip

Ask the agent for a deletion pass after a feature works: “Find every parameter, interface, class, branch, and test that exists only for a future case. Remove it unless you can point to a current caller or requirement.” The best time to delete speculative generality is before the team starts depending on its shape.

How It Plays Out

A developer asks an agent to add CSV export to an admin screen. The agent builds an ExportProvider interface, a CsvExportProvider, a factory, a registry, and a configuration file that selects the provider by name. There is one export format, one caller, and no roadmap item for another. The team deletes the provider layer and ships one function that writes CSV. Six months later, when PDF export becomes real, they design the second path around the actual PDF requirements instead of the guessed provider API.

A team builds a webhook receiver for one partner. A senior engineer adds a partner_type parameter, a base PartnerAdapter, and a test fixture for “future partners.” Every production call passes "acme". Two years later, a second partner arrives with asynchronous callbacks and a different signature scheme. The adapter interface cannot express either difference cleanly. The team has to break the abstraction and update every test that had been preserving the fantasy version of multi-partner support.

An agent refactors a parser and notices two branches that look similar. It creates an abstract TokenSource class with one subclass and a mode flag that no caller changes. The refactor passes tests, but future prompts now see a design that invites extension. The next agent adds a second fake mode because the API suggests one should exist. A human reviewer finally asks what the second mode is for, and the answer is nothing. The right refactor was a smaller parser with better names, not a family of sources.

Consequences

Removing speculative generality keeps the code close to the requirement. Review gets faster because readers no longer have to model imagined branches. Tests get sharper because they protect behavior that can actually happen. Agents perform better because the local code teaches them the real shape of the system rather than a set of unused options.

The liability is that some changes will require a later refactor. That is not a failure. A later refactor guided by two real cases is usually cheaper than years of maintaining the wrong abstraction. The discipline isn’t anti-design; it’s anti-guessing. Design for change by keeping the roots shallow, the tests honest, and the current behavior clear.

Some early generality is justified. Public APIs, data formats, migration paths, and security boundaries can be expensive to change after release. Treat those as requirements, not guesses: write down the constraint, name who depends on it, and test the compatibility promise. The antipattern begins when the only evidence is anxiety about a future no one has committed to.

Sources

  • Martin Fowler and Kent Beck’s Refactoring: Improving the Design of Existing Code names Speculative Generality as one of the classic code smells and credits Brian Foote for the name. Their treatment gives the concrete removal moves this article uses: collapse unused hierarchies, inline unnecessary delegation, remove unused parameters, and delete dead code.
  • Fowler’s bliki entry on Code Smell defines a smell as a quick surface indication of a deeper problem rather than a guaranteed bug. Speculative generality fits that definition: the unused hook is the smell, and the deeper problem is design based on guesses rather than requirements.
  • Fowler’s Yagni essay and the XP community’s You Arent Gonna Need It page frame the corrective heuristic: implement features when you actually need them, because guessed future needs often turn out wrong or arrive in a different form.