# Speculative Generality

> Abstractions, hooks and parameters built for future needs that never arrived make code harder to read without serving any current use.

- kind: smell
- category: dispensables
- aliases: YAGNI violation, Over-engineering
- canonical: https://aipatterns.dev/refactoring/smells/speculative-generality
- inspired-by: https://refactoring.guru/smells/speculative-generality

## Signs

- An interface has exactly one implementation, and no test double uses it either.
- An abstract base class has one subclass.
- A function takes options or parameters that every caller passes with the same value, or never passes.
- A plugin system, registry or strategy hook exists with one plugin.
- Names are generic (`DataProcessor`, `handleEntity`, `GenericRepository<T>`) for code that handles one concrete thing.
- The only callers of some methods are their own tests.

## Why it happens

Developers anticipate change and build the flexibility up front, often after being burned by a rigid design. The anticipated change rarely arrives in the shape that was predicted, so the abstraction ends up both unused and wrong. AI agents are prone to this smell too, because generating extra layers costs them nothing.

## Example

A notification module was built for "any channel" but only ever sends email.

```ts title="notify.ts"
interface Channel<TPayload> {
  send(payload: TPayload, options?: { priority?: number; retry?: boolean }): Promise<void>;
}

abstract class BaseChannel<T> implements Channel<T> {
  abstract send(payload: T, options?: { priority?: number; retry?: boolean }): Promise<void>;
}

class EmailChannel extends BaseChannel<{ to: string; body: string }> {
  async send(p: { to: string; body: string }) {
    await smtp.send(p.to, p.body);
  }
}

class ChannelRegistry {
  private channels = new Map<string, Channel<any>>();
  register(name: string, c: Channel<any>) { this.channels.set(name, c); }
  get(name: string) { return this.channels.get(name)!; }
}

const registry = new ChannelRegistry();
registry.register("email", new EmailChannel());
await registry.get("email").send({ to: "a@example.com", body: "Your order shipped" });

declare const smtp: { send(to: string, body: string): Promise<void> };
```

The `priority` and `retry` options are never read. Collapse to what is used:

```ts title="notify.ts"
export async function sendEmail(to: string, body: string): Promise<void> {
  await smtp.send(to, body);
}

await sendEmail("a@example.com", "Your order shipped");

declare const smtp: { send(to: string, body: string): Promise<void> };
```

When SMS arrives, you will know its real shape and can introduce an abstraction that fits both.

## Treatment

- If an abstract class has one subclass, use [Collapse Hierarchy](https://aipatterns.dev/refactoring/techniques/collapse-hierarchy.md).
- If a class only delegates or wraps without adding value, use [Inline Class](https://aipatterns.dev/refactoring/techniques/inline-class.md).
- If a method only forwards to another, use [Inline Method](https://aipatterns.dev/refactoring/techniques/inline-method.md).
- If a parameter is unused or always the same value, use [Remove Parameter](https://aipatterns.dev/refactoring/techniques/remove-parameter.md).
- If a method has an abstract, future-proof name, use [Rename Method](https://aipatterns.dev/refactoring/techniques/rename-method.md) so it says what it does today.
- Delete tests that only exist to exercise the unused generality.

## Payoff

- Code says what the system does, not what it might do.
- Fewer types and indirections to follow.
- When the real second case arrives, you design the abstraction from two concrete examples instead of guessing.

## When to ignore

- You are building a framework or library whose users supply the other implementations.
- The interface is a test seam: tests substitute a fake, and that fake counts as a second implementation.
- The second implementation is committed work in the current iteration, not a "someday".
- The abstraction isolates a third-party dependency you are contractually or operationally likely to swap.

## Agent checklist

- [ ] For each abstraction you removed, you confirmed there is no second implementation in tests, other packages or plugins.
- [ ] Parameters you removed were unused or constant at every call site.
- [ ] You did not delete an extension point that is part of a public API.
- [ ] The simplified code passes the same tests, minus tests that only covered the removed generality.
- [ ] You did not add new "for later" flexibility while simplifying.
