---
name: generalize
description: Removes hardcoding and overfitting by promoting special cases into data, parameters, and honest abstractions — at the right moment. Detects the smells (magic literals, one-case branches, single-fixture logic, "it works for the example"), generalizes the minimum that kills them, and stops before speculative generality. Use when code works for one input/instance/environment but must handle a family of them, when review says "hardcoded", or when a fix smells overfit to the bug report.
category: implementation
keywords: generalization, hardcode, overfitting, magic-numbers, parameterize, data-driven, rule-of-three, abstraction, configuration, special-case
---

# Generalize

Overfitting is fitting to the *example* instead of the *population* — in ML, in code, in fixes. A hardcoded value is a decision frozen in the wrong place; a one-case branch is a rule that pretends it's an exception. Generalizing means moving each frozen decision to where it can vary: a parameter, a table, a token, a loop over data. The discipline is bidirectional — generalize too late and the code is brittle; too early and you ship speculative generality (an abstraction with one user is a tax, not a tool).

## When to apply

Use when code is correct for the case in front of you but visibly borrowed from it: literal values from the sample input, a branch shaped exactly like one customer/environment/format, a fix that passes the bug report's repro and nothing else. Also use on request: "this is hardcoded", "don't overfit to this fixture", "make it handle the general case".

## Smells that say "overfit"

- **Magic literals with provenance** — `if (x === 37)`, a `+2` pixel fudge, a timeout copied from the repro. The value encodes an observation, not a rule.
- **One-case branches** — `if (env === "staging")` where staging is the only env seen so far; the branch enumerates instances instead of expressing the dimension.
- **Fixture-shaped logic** — parsing that only handles the exact structure of the one sample file; a test suite of one happy path; regex anchored to a single example.
- **Copied blocks differing by a literal** — three near-identical lines where only a value changes; the value is the loop variable you didn't write.
- **Fix overfit to the report** — the patch makes repro #1 pass by special-casing its exact inputs, leaving the underlying rule unfixed.
- **Environment/instance leakage** — absolute paths, hardcoded hostnames, IDs, names, dates, locale assumptions, or model names baked into logic.

## The generalization ladder

Climb one rung at a time; each rung is cheaper to review than the one above:

1. **Name it** — extract the literal into a named constant *where it's used*. Zero structure change; kills the magic.
2. **Parameterize it** — hoist the constant to a function argument or module config. The caller decides; the rule stays one shape.
3. **Make it data** — replace instance-branching with a table: map/config/array of records, iterated by one generic path. `if type === "a" … if type === "b" …` becomes rows in a map. Most "generalization" ends here, well before an abstract class.
4. **Extract the rule** — only when ≥2–3 implementations genuinely differ in *behavior* (not just values) introduce an interface/strategy. Rule of three: tolerate one duplicate, extract at two, demand a third before anything fancier.
5. **Stop.** — a parameter nobody passes, a plugin point with one plugin, a config key nobody sets — that's speculative generality; delete it (see `engineering-roast`'s classic hits).

## Workflow

1. **Collect the population** — before generalizing, gather ≥2–3 real instances: sample inputs, environments, customers, formats. One instance is an anecdote; generalizing from it is how you launder a guess into a rule. If only one instance exists, say so and generalize only rungs 1–2.
2. **Find the invariant** — across instances, name what stays the same (the shape of the rule) versus what varies (the values, the rows). The invariant becomes the code; the variance becomes data.
3. **Choose the rung** — pick the lowest ladder rung that eliminates the smell. Prefer data over interface, constants over parameters, deleting over either.
4. **Preserve the concrete case** — the original instance must still pass unchanged. Add the new instances as tests in the same change; a generalization with no second example is unproven.
5. **Prove the family, not just members** — where feasible, test the dimension itself (loop over the table, property-test the parameter range) so the next instance arrives as a new row, not a new branch.
6. **Sweep for siblings** — grep for the same literal/smell elsewhere; generalized code with five hardcoded cousins is half a refactor.
7. **Record the decision** — one line in the config/table/README stating what varies and what's intentionally fixed. The next reader should learn the dimension from the code, not archaeology.

## Rules

- No generalization from a single instance — name it or parameterize it, but don't abstract it.
- Data beats structure — a table entry is cheaper than a subclass, always.
- Every generalized path keeps a passing concrete case — unproven generality is a guess with types.
- The fix must not be a lookup of the bug report — if the patch only answers repro #1, it's overfit.
- Generality you can't point to a second consumer for is debt — climb down a rung.

## Related skills

- [lean-implementation](/skills/lean-implementation) — The counterweight: don't generalize past the problem at hand.
- [minimal-fix](/skills/minimal-fix) — Fix the rule, not the instance; how to shape the non-overfit patch.
- [engineering-roast](/skills/engineering-roast) — Roasts speculative generality, the overcorrection of this skill.
- [round-trip-testing](/skills/round-trip-testing) — Property-style corpora that test the family instead of members.
