What is the "speculative generality" code smell, and how do the Rule of Three and Sandi Metz's maxim "duplication is far cheaper than the wrong abstraction" help you decide when to introduce an abstraction?
answer
- Fowler smell: interface with one implementation
- Rule of Three — Don Roberts; wince, then refactor
- Metz: wrong abstraction > duplication in cost
- Remedy for wrong abstraction: re-inline, then re-extract
- Deduplicate reasons to change, not characters
basics
~20 sSpeculative generality is machinery built for a future that never arrives — an interface with one implementation, unused parameters, hooks nobody calls. Rule of Three: wait for the third occurrence before abstracting. Until then, duplication is safer than guessing the wrong shared shape.
solid answer
~50 sSpeculative generality (a smell named in Fowler's *Refactoring*) is any construct that exists only to support imagined future needs: single-implementation interfaces, abstract base classes with one subclass, unused parameters or hooks, factories and registries with one entry, "just in case" configuration. It costs on every read and every change, forever, while delivering nothing today. The Rule of Three (Don Roberts, quoted by Fowler) is the heuristic antidote: duplicate once without guilt; on the third occurrence you have enough evidence about what actually varies to factor out the right thing. Sandi Metz sharpens the trade-off: a wrong abstraction is worse than duplication, because callers bend to fit it — flags, conditionals and special cases accrete inside it until nobody dares change it, and unwinding it is far harder than deduplicating three copies. So the decision rule is evidence-based: abstract when the repetition represents *the same reason to change* (one concept), not merely the same characters on screen.
code
pseudocode · 10 lines// Speculative generality: one implementation, one caller, no second case in sight
interface PricingStrategy { price(order): Money }
class DefaultPricingStrategy implements PricingStrategy { ... }
class PricingStrategyFactory { create(name): PricingStrategy { return DefaultPricingStrategy() } }
checkout() { PricingStrategyFactory().create(config.strategy /* always "default" */).price(order) }
// KISS/YAGNI version until a real second strategy exists
checkout() { priceOrder(order) }
// When the third variant genuinely appears, the shape of the variation is now known
// and the extracted seam is evidence-based rather than guessed.go deeper
Recognise and name the smell (interface with one implementation, unused parameters, flags with one value) and state the Rule of Three as your default. Say that duplicating twice is acceptable.
Explain why two samples aren't enough evidence, describe the carry costs of an unnecessary seam, and reproduce Metz's failure sequence where flags accumulate inside a shared abstraction.
Reframe the rule as "deduplicate reasons to change, not characters", distinguish coincidental from real duplication, and name the exceptions: public APIs, persisted formats, high-risk logic, and genuine test seams at external boundaries.
Discuss how you set organisational defaults: concrete-first inside a service, evidence-based extraction, and a higher design bar for cross-team contracts. Cover how to detect and pay down wrong abstractions that have become de-facto platform coupling.
## The smell **Speculative generality** appears in Martin Fowler's *Refactoring* catalogue of code smells. Definition: code exists solely to handle things you *think* you might need someday. Recognisable forms: - an interface or abstract class with exactly one implementation, created "so we can swap it later" - a factory / registry / plugin loader with one registered thing - parameters, options objects, or callbacks never supplied by any caller - generic type parameters instantiated at exactly one type - configuration keys with one possible value; feature flags never flipped - an abstraction layer over a dependency (database, message broker, cloud SDK) to make it swappable, when no swap is planned or budgeted - extension hooks (`beforeX`, `afterX`) with no subscribers Fowler's prescribed refactorings are the collapsing ones: *Collapse Hierarchy*, *Inline Class*, *Inline Function*, *Remove Parameter*, *Rename* the survivor to a concrete name. ## Why it costs more than it looks The build cost is small and visible; the **carry cost** is invisible and recurring: 1. **Reading tax** — every future reader must trace one extra hop to answer "what actually runs?", and must consider whether other implementations exist. 2. **Navigation tax** — jump-to-definition lands on the interface, not the code. 3. **Change amplification** — adding a method means touching interface + impl + mocks + tests. 4. **False signal** — an interface advertises "multiple implementations exist / are expected", so people write defensively for cases that don't exist. 5. **Wrong-shape lock-in** — the guessed seam is usually in the wrong place, so when the real second case arrives it doesn't fit, and you now pay to *remove* the abstraction as well as to build the right one. 6. **Test rot** — mocks against a speculative interface test the mock, not the behaviour. ## The Rule of Three Attributed to Don Roberts and popularised by Fowler: *"The first time you do something, you just do it. The second time you do something similar, you wince at the duplication, but you do the duplicate thing anyway. The third time you do something similar, you refactor."* Why three: with one instance you have zero information about what varies. With two, you cannot tell which differences are incidental and which are the axis of variation — any abstraction you extract is a coin flip. With three you can see the *pattern of variation*: what stays fixed across all three is the abstraction's body; what differs is its parameters. It is a heuristic, not a law. Override it when: the duplication is in something expensive to get wrong (security checks, money arithmetic, a wire format), when copies will inevitably drift and drift is dangerous, or when the concept is already well-known and stable (you don't rediscover "paging" three times). ## "Duplication is far cheaper than the wrong abstraction" Sandi Metz's formulation, from her talk/post on the wrong abstraction. Her described failure sequence: 1. Someone extracts a shared abstraction from real duplication — correct at the time. 2. A new requirement arrives that *almost* fits. The cheapest local move is to add a parameter/flag and a conditional inside the abstraction. 3. Repeat. The abstraction accumulates flags, and each caller passes a slightly different combination. 4. Now the code is coupled to *all* callers, nobody understands the flag matrix, and changing it risks breaking unrelated features. Reading it is harder than reading four separate copies would have been. Her prescribed remedy when you inherit this: **re-inline** — push the abstraction's code back into each caller, delete the parts each caller doesn't use, and only then look for the real (probably different) abstraction. Duplication is a *local, cheap* mistake; the wrong abstraction is a *global, coupling* mistake. ## The real decision rule Don't deduplicate text; deduplicate **reasons to change**. Two snippets that look identical but change for different reasons (a validation rule for orders and an identical-looking one for invoices, owned by different teams) are *coincidental* duplication — merging them couples two independent futures. Two snippets that must always change together are one concept, and belong in one place. This is DRY read correctly: DRY is about a single authoritative representation of a piece of *knowledge*, not about never typing similar characters twice. ## Interaction with KISS and YAGNI Speculative generality is exactly what YAGNI forbids, expressed structurally. Rule of Three operationalises YAGNI for abstraction: don't build the extension point for the second case until the second (really the third) case exists. KISS says the abstraction you do build should have the fewest knobs that satisfy the observed cases. Together they produce evolutionary design: concrete first, abstraction extracted from evidence. ## Counter-pressures to name - **Public/shared boundaries**: for an API other teams consume, or a persisted format, guessing wrong is expensive to undo — spend more design effort there. - **Testability seams**: needing to substitute a component in tests is a *present* need, not speculation. An interface introduced because you must fake a payment gateway today is justified by today's requirement. - **Cheap, standard patterns**: using a well-understood pattern that costs nothing extra to read is not speculative generality.
- You have exactly two near-identical blocks and a deadline. Extract now or duplicate?Default to duplicating and leaving a marker, because with two samples you cannot tell incidental differences from the real axis of variation. Override that default if the logic is high-risk to have drift in — money, security, a wire format — where a single authoritative copy is worth the guess.
- How do you unwind an abstraction that has collected a pile of boolean flags?Inline it back into each call site, then delete from each copy the branches that call site never takes. With the concrete cases visible again, re-extract only what genuinely shares a reason to change — often a smaller helper than the original, or several distinct ones.
- Isn't an interface with one implementation justified if you need to mock it in tests?If substituting it in a test you are writing today is a real requirement, the seam is justified by present need, not speculation. But prefer seams at genuine external boundaries (network, clock, filesystem); adding interfaces over pure in-process logic to enable mocks usually signals the test is the wrong shape.
Building a house with a doorway framed for a future extension you might add: it costs little to frame, but every year you walk past a door to nowhere, repaint it, and route the wiring around it. Worse, when you finally build the extension, it's on the other side of the house.