Lesson 0030 · Master Designer · Module 1
Generality vs Specificity
A general module is a bet on the future; a specific module is a fact about today. Ousterhout's law: generality is earned when it makes the specific case simpler — not when it adds cases.
Mission tie-in: the last Complexity Trade-Off — and the one that decides how much machinery every module carries.
Knowledge: generality that pays vs generality that costs
Two kinds of generality:
- Earned generality — the module solves today's case through a shape that naturally covers tomorrow's. It makes the current code simpler because the general interface is cleaner than the specific one.
- Speculative generality — parameters, hooks, and abstractions for cases that don't exist. It taxes every read of the current code to pay for futures that may never arrive.
# speculative: a "pipeline" framework for one transformation
class Transformer:
def __init__(self, steps: list[Step], concurrency: int = 1,
retry: RetryPolicy | None = None, metrics: Metrics | None = None):
...
Transformer(steps=[normalize], concurrency=1, retry=None, metrics=None).run(data)
# earned: the general name covers the specific case, no extra machinery
def transform(data: list[dict]) -> list[dict]:
return [normalize(row) for row in data]
The specific version is also the general one: any future step is a new function, composed (lesson 0016). The framework added configuration (lesson 0029), concurrency, and retries — none of which the one case needs.
The test, from Ousterhout: does the generality make the current problem simpler? If the general module is harder to use for today's case than the specific one would be, the generality is speculative. Generalize only when the second use case is real — then the general shape is usually visible (rule of three again).
† AI's default is speculative generality: ask for "a function" and get "a configurable framework". The prompt discipline from lesson 0010 — "solve this case, no parameters beyond it" — is the countermeasure.| You see this | What it costs | The move |
|---|---|---|
| a default that no caller ever overrides | Generality nobody asked for, read by everybody. | Delete the parameter; hardcode the one value. |
| **kwargs forwarded “for flexibility” | The contract is now unknowable without reading the callee. | Name the arguments you actually accept. |
| an interface with one implementation | Indirection with nothing on the other side to vary. | Call the concrete thing until case two exists. |
| a plugin system with two in-tree plugins | Machinery for an ecosystem that does not exist. | Two functions and a lookup. |
Skill: earned or speculative?
Generality is earned when it:
A one-use transformation wrapped in a "pipeline framework" is:
The honest trigger for generalizing is:
Practice on your own code
Find a module with parameters or machinery no caller uses (defaults never overridden, hooks never hooked, config from lesson 0029 that's always the same). Remove the machinery — make the specific case the whole module — and check that every caller got simpler.
Reveal: a de-generalization
A PaymentProcessor accepted webhook_url: str | None and idempotency_store: ...; every caller passed the same values. De-generalized: the webhook and idempotency behaviors became hardwired parts of the processor's contract; the parameters vanished; the constructor shrank from six lines of options to two required arguments.
Your win
You can distinguish earned from speculative generality with the simplification test, and strip unearned machinery without losing capability.
Read and watch deeper
- A Philosophy of Software Design, Ousterhout — ch. 6 "General-Purpose Modules Are Deeper": the simplification test this lesson uses.
- "Simple Made Easy", Hickey — the distinction between complexity carried and complexity deferred.
- Refactoring, Fowler — the "speculative generality" smell entry.
- Watch: ArjanCodes YouTube — search "YAGNI" or "simplicity".
- Next module: lesson 0031 — System Trade-Offs.
- Reference: Trade-offs — generality vs specificity; glossary — speculative generality.
Bring a never-overridden parameter to your agent-teacher and de-generalize it together.