Speculative Generality
Machinery built for a variation that never arrived: one implementation behind an interface, a plugin system with no plugins, an event bus for a local call.
The requirement, the obvious build, and why it breaks
Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind survives until the requirement changes.
How do I tell structure that is paying for itself from structure built for a future that never came?
A new engineer asks why adding a field to the signup form requires editing seven files across four packages. Nobody on the current team wrote any of them, and every layer appears to be passing the same data along unchanged.
It is over-engineered but harmless. It works, and removing it is risk with no visible payoff, so leave it alone and route around it.
It is not harmless: every new engineer pays the cost of understanding it, every change threads through it, and every one of those costs recurs forever while the benefit stays at zero (Local Reasoning).
- It is not harmless: every new engineer pays the cost of understanding it, every change threads through it, and every one of those costs recurs forever while the benefit stays at zero (Local Reasoning).
- Routing around it is how a codebase ends up with two shapes — the ceremonial path everybody follows and the direct path people use when in a hurry — and reasoning about a system with two shapes is harder than either.
- The machinery attracts more of itself. A new feature is written to match the surrounding style, so an unnecessary abstraction reproduces every time somebody adds a case (Patterns as Vocabulary).
- The cost of removal grows with time, because each new caller is another migration. "Leave it alone" is not a neutral choice; it is choosing the more expensive removal later (Interest: Why Debt Compounds).
What limits the solution, and what must never stop being true
This domain leads with these two. A design that ignores its constraints is not a design, and an invariant nobody named is one nothing is protecting.
- The machinery works. Nothing is broken, so there is no incident to justify the time to remove it.
- Some of it is load-bearing in ways that are not obvious, so bulk deletion is not safe (Refactoring Without Tests).
- The original authors are gone, so the intent behind each layer has to be inferred from the code and the merge history.
- Removing structure never changes behaviour. If it does, it was not a refactoring and the removal has to be re-planned (What Refactoring Actually Is).
- Every structure that survives the review can name a change it makes cheaper, and that change has to be one somebody actually expects.
Who owns what, and where the seams fall
Responsibilities decide boundaries; boundaries decide what an interface has to say.
- Whoever adds structure owns naming the change it makes cheap, at the moment of adding, where it is a sentence rather than an archaeology project.
- Code review owns asking the question. It is the only checkpoint where the answer is cheap to get (A Review Checklist Worth Reading).
- Whoever finds unused machinery owns either deleting it or writing down why it stays — an unexamined layer that survives a review has been silently promoted to a decision.
- The boundary that matters is between structure justified by an observed requirement and structure justified by a hypothetical one. Only the second is this smell.
- A layer that transforms — validating, mapping between two genuinely different models, enforcing a rule — is doing work. A layer that passes its input to the next layer unchanged is not a boundary, it is a hop (Boundary Adapters).
- One implementation behind an interface is not automatically wrong: a test double, a compile-time seam for a dependency you must not touch in tests, or a module you genuinely intend to replace are all real reasons (Test Doubles, Precisely).
The smell, and when the same code is right
This is the one smell in the domain most likely to be applied as a verdict, and it is the one where the correct code and the defective code look identical. An interface with a single implementation tells you nothing on its own. The question is always what justified it.
The five silhouettes below are the common ones. Each has a legitimate version, which is why the diagnostic is a conversation with the merge history rather than a lint rule.
- The diagnostic question is not "does this have one implementation" but "what would have to become true for a second one to exist, and does anyone expect it".
- A structure whose justification is "flexibility" has no justification, because it names no change (Evolvability).
- A structure whose justification is "the SDK cannot be constructed in tests" has a very good one, and that sentence should be in the code.
looks like An interface with exactly one implementation and no test double. A plugin registry with no plugins outside the repository. An event bus whose only publisher and only subscriber are in the same module. A factory that always constructs the same class. Four layers in which a request object is renamed and passed along unchanged. Type parameters that are only ever instantiated at one type.
suggests The structure was built for a variation that was anticipated rather than observed, and the variation did not arrive. Its cost — indirection on every read, ceremony on every change, and a shape that new code copies — is being paid continuously against a benefit of zero.
fix Find the evidence first: search the whole repository and the merge history for a second implementation, a second subscriber, an external plugin. If there is none and nobody can name an expected one, inline the indirection so the code reads directly, verify behaviour with characterization tests, then delete the abstraction — one layer per pull request. If it stays, write down why in one sentence next to the definition, so the next engineer does not repeat this investigation (Decision Records).
Layers that pass data along unchanged
The most expensive variety is the one that does not look like an abstraction at all: a stack of layers in which each one receives a shape, renames it, and hands it on. It is expensive because it is where the seven-file field addition comes from, and because each layer individually looks like diligence.
The test is whether a layer makes a decision. Validation is a decision. Mapping between a wire format and a domain model is a decision, because the two must be able to change independently. Copying six fields into an identically-shaped object with different names is not.
1// http/SignupController.ts2const dto = new SignupDto(req.body.email, req.body.name)3return this.service.signup(dto)4 5// app/SignupService.ts6signup(dto: SignupDto) {7 return this.manager.create(new SignupCommand(dto.email, dto.name))8}9 10// domain/SignupManager.ts11create(cmd: SignupCommand) {12 return this.repo.save(new UserEntity(cmd.email, cmd.name))13}14 15// Adding `referralCode` edits four files plus four tests16// plus two mapper suites, and not one of those edits17// contains a decision.The fix is not "delete all layers". It is to notice that exactly one boundary here is doing work — turning untrusted request data into a validated domain value — and that it can happen once, at the edge. Everything after that can pass the domain type directly (Parse, Do Not Validate is the backend name for the same move). What you keep is the boundary where the decision is; what you remove is the three renames.
What the machinery costs the change it was built for
The strongest argument against speculative structure is not that it wastes the build effort. It is that when a real requirement finally lands nearby, the machinery makes *that* requirement more expensive, because the change has to be threaded through a shape designed for something else.
Here is the same requirement priced against the ceremonial codebase and against the direct one. Note the cost line: the direct version is not free, and pretending otherwise is how teams over-correct into having no boundaries at all.
Signup accepts an optional referral code, validates it against active campaigns, stores it on the account, and includes it in the welcome email.
Ten files, eight suites, and one decision — validating the code against active campaigns. Everything else is the same six characters typed in different casings, and the risk is that one of the ten is missed and the field silently arrives as undefined.
Three modules, and the two that changed are the two that contain the actual requirement: parse and validate the input, and check it against campaigns. Adding the next optional field costs one type and one test.
How to build it
Most important first.
- Ask the question at the point of addition, not later: which change does this make cheaper, and who expects that change? A structure that cannot answer does not go in (The Cost of Change).
- When you find existing machinery, look for evidence before judging: search for a second implementation, a second caller, a plugin, an external subscriber. Absence of all of them across the merge history is the finding.
- Inline before deleting. Collapse the indirection so the code reads directly, verify behaviour is unchanged, then remove the now-unused abstraction (Extract Function in reverse).
- Remove one layer at a time and ship between removals, because the failure mode of a big cleanup is that it becomes unreviewable and gets abandoned half-done (Incremental Migration).
- Write the reason down when structure stays. "This interface has one implementation because the payment SDK cannot be constructed in tests" ends the conversation permanently (Decision Records).
What the next change costs
The field this whole domain exists for. A structure is only better if it makes the change after this one cheaper — and it is worth saying which changes it does not help.
- With the machinery: adding a field costs seven files, four packages and a review from two teams. None of those edits contains a decision — they are all restatements of the same field name.
- Without it: the same change costs one module and its test. The saving is not the six files; it is that the change no longer requires knowing the ceremony.
- Removing it costs a migration for every current consumer, and that cost rises every month it is left in place — which is the argument for doing it now rather than the argument for having done it then.
- The one change that gets more expensive after removal is the hypothetical one the machinery was built for. If it ever arrives, you rebuild — with the advantage of knowing what it actually needs.
- Removal is risk applied to working code for a benefit that is diffuse and shows up only in future changes — the hardest kind of work to justify in a sprint plan.
- A codebase with less structure is more exposed if the anticipated variation does arrive, and occasionally it does. That is a real loss and the strongest thing the other side can say.
- Asking "which change does this make cheaper" in every review adds friction, and applied without judgement it becomes a way for a reviewer to block work on principle (Tone, Disagreement and Receiving Review).
What can go wrong
- The interface is deleted and it turns out one implementation was swapped in a single production configuration nobody grepped for.
- The removal is done as one enormous pull request, becomes unreviewable, and is abandoned — after which the codebase has the machinery plus a failed attempt in its history, and nobody tries again.
- The team over-corrects and starts removing structure that is load-bearing, on the grounds that it "looks like" speculative generality; the tell is that they stop asking which change each piece makes cheap and start pattern-matching on shapes (Over-Design and Under-Design).
- The machinery is removed and re-added six months later by someone applying the same reasoning that produced it the first time, because the decision record was never written.
- Every consumer of the machinery is a reason it is hard to remove, which is why removal cost rises with age rather than falling (Reversible and Irreversible Decisions).
- Speculative structure often depends on framework features — dependency-injection containers, event buses, code generation — so removing it also means removing a framework dependency, which raises the perceived stakes (What a Framework Charges).
- Tests written against the machinery rather than against behaviour become a dependency on the machinery, and are frequently what blocks the cleanup (Mocking).
- "Any interface with one implementation is a smell." Frequently it is the right code: a seam for an untouchable dependency, a boundary that is genuinely a contract, a module you are actively replacing. The smell is an interface whose *justification* is a hypothetical second implementation (Interface Versus Implementation).
- "So we should always write the simplest thing." The counterexamples matter: a tenancy boundary, an id scheme or an audit trail is much cheaper built early than retrofitted, and none of those is speculative just because it is not needed today (The Cost of Change).
- "This is technical debt." Debt is a choice that trades future cost for present speed. Speculative generality bought no speed at all — it cost time to build *and* costs time forever — so it is not debt, it is waste, and the distinction matters because the remedies differ (What Technical Debt Actually Is).
- "More layers mean better separation of concerns." Layers that pass data through unchanged separate nothing; they add hops. Separation requires that each layer make a decision the others do not (Separation of Concerns).
- code-smells
- utility-dumping-ground
Testing it, and how it ages
- Characterize the behaviour of the path you are about to collapse, at the outermost stable boundary, before touching anything (Characterization Tests).
- Delete tests that assert the machinery rather than the behaviour, at the same time as the machinery. Keeping them turns the cleanup into a test rewrite and stalls it (Testing as Design Feedback).
- After each removal, run the full suite and diff a production-like request end to end, because the risk here is a subtle behavioural difference rather than a crash.
- Speculative structure is stable in the worst sense: nothing forces it to change, so it survives every reorganisation and every rewrite of the code around it.
- It gets progressively harder to identify as the codebase grows, because new engineers assume anything that old must be there for a reason — which is exactly how it acquires authority it never earned.
- The natural moment to remove it is when a real requirement lands on the same area, since you are already there, already have the tests warm, and can compare what the requirement needs against what the machinery offers.
Where this applies
This domain's advice is contested more than most. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view rather than a caricature.
- GENERALThat structure built for unobserved variation charges indirection with no return holds regardless of language. What changes is the local price of an abstraction — a Java interface, a Go interface satisfied implicitly, and a Python protocol impose noticeably different reading costs for the same design.
- PARADIGM-SPECIFICIn an OO codebase this smell usually appears as interfaces, factories and inheritance hierarchies with one branch. In a functional one it appears as premature parametric polymorphism, higher-order functions taking configuration nobody varies, and type classes with one instance. Same mistake, different silhouette, and a reviewer trained on one often fails to see it in the other.
- CONTESTEDThe strongest defence of what this lesson calls speculative: consistency has real value, and a team that applies a uniform shape everywhere — every use case behind an interface, every module wired the same way — gets a codebase any engineer can navigate without learning local conventions, which is worth more at fifty engineers than the indirection costs. That argument is genuinely strong at scale and in codebases with high staff turnover. It is much weaker at five engineers, where the uniformity is being paid for and nobody is arriving who needs it.
Where the depth lives
This domain teaches the codebase-level structure and hands the rest off.
- — Programming Languages & Runtime Internals — what an interface, a virtual call or a generic instantiation actually costs at runtime, which sets a floor under the indirection tax that this lesson treats as a reading cost.