Skip to content

Anti-over-abstraction Rule

Spirit (the 5 non-negotiables)

  • Start with the smallest direct solution: implement the current requirement with concrete code that is easy to trace.
  • Require present evidence: an abstraction needs a real boundary, meaningful reuse, multiple implementations, or a measurable reduction in complexity now.
  • Accept small, local duplication: similar code may represent concepts with different reasons to change.
  • Use existing foundations: prefer the framework, standard library, and established project APIs over parallel infrastructure.
  • Optimize total cognitive load: judge definitions, bindings, call sites, tests, configuration, and navigation together—not one file in isolation.

Rules

1) Prefer direct, concrete implementations

Write the narrowest implementation that satisfies the current behavior and constraints. Keep a simple flow inline when its purpose is clear.

Do not add a helper, wrapper, service, repository, interface, factory, or base class merely to give a name to a few lines.

2) Require evidence before introducing an abstraction

An abstraction is justified only when at least one of these is true:

  • It represents a real, named domain concept or architectural boundary.
  • Multiple current callers share the same behavior and the same reason to change.
  • Multiple current implementations must be substituted behind one contract.
  • It removes meaningful repeated complexity, not just a few repeated lines.
  • It isolates an unstable external service, protocol, or vendor boundary.
  • It materially improves testing where framework-native testing is insufficient.

Imagined future callers and hypothetical implementations are not evidence.

3) Small duplication is allowed

Do not extract code simply because a method is long or two snippets look similar. Follow the rule of three as a heuristic, not a command: first identify whether the code has a stable shared meaning and a stable shared reason to change.

Keep code local when extracting it would add more concepts, indirection, or navigation than it removes.

4) Do not design for requirements that do not exist

Do not introduce speculative configuration, extension points, plugin systems, callbacks, strategy registries, or generic type parameters for hypothetical future needs.

Implement the requirement that exists. Expand the design when a concrete requirement and evidence arrive.

5) Use framework and standard-library primitives directly

Before creating custom infrastructure, check whether the framework or standard library already provides the capability. Use established project APIs directly when they already own the responsibility.

A wrapper is justified only when it adds substantial policy, translates between genuine boundaries, centralizes unstable vendor behavior, or provides a project-wide contract with more than one meaningful consumer. Renaming methods or forwarding arguments is not enough.

6) Prohibited speculative layers

Do not create these for one straightforward use case unless the task or existing architecture clearly requires them:

  • Generic helpers or utility classes.
  • Wrappers around framework APIs.
  • Service or manager classes that only forward calls.
  • Repositories that merely mirror an ORM.
  • Interfaces with one implementation and no real substitution boundary.
  • Factories that only call one constructor.
  • Base classes or traits used only to share a few lines.
  • Pipelines, registries, strategies, adapters, DTOs, or command buses added for ceremony.
  • Custom validation, routing, events, queues, caching, storage, collection, dependency-injection, or ORM infrastructure already provided by the framework.

7) Prefer local composition over global indirection

Use a small private method when it clarifies one local flow. Do not promote it into a global helper without multiple real consumers. Keep domain policy separate from infrastructure when that boundary is real; do not split simple conditionals into polymorphism unless the variants are numerous, independently evolving, or externally supplied.

8) Preserve contracts and reuse existing boundaries

Keep existing public behavior and project conventions unless the requirement explicitly changes them. Reuse an abstraction that already owns the responsibility instead of creating a competing layer.

Do not add a test-only production abstraction. Test the direct implementation at the closest useful level, using framework-native testing facilities where possible.

Review questions

Before approving or introducing an abstraction, ask:

  1. What current requirement does this abstraction satisfy?
  2. Which qualifying evidence supports it: a real boundary, multiple consumers, multiple implementations, meaningful repeated complexity, or a testing limitation?
  3. What is the direct inline alternative?
  4. Does the abstraction reduce total complexity after counting files, concepts, bindings, call sites, mocks, tests, and configuration?
  5. Does it wrap or rebuild a capability already provided by the framework or standard library?
  6. Are the callers and implementations current, or only imagined future needs?
  7. Can the new helper, interface, service, factory, trait, or configuration option be deleted while keeping the code clear?

Implementation checklist

  • [ ] Inspect nearby code and project conventions first.
  • [ ] Identify the exact current behavior and constraints.
  • [ ] Implement the narrowest framework-native solution.
  • [ ] Name the concrete evidence before adding an abstraction.
  • [ ] Compare the direct and abstracted versions by total cognitive load.
  • [ ] Preserve existing public contracts.
  • [ ] Remove unused pass-through layers and speculative options.
  • [ ] Add only the closest useful tests; do not add test-only production abstractions.

When uncertain, keep the code inline and concrete.

Internal engineering documentation