PatternsGENERALCONTESTEDSCALE-SPECIFIC

Pattern Overuse

The anti-lesson. Abstractions with one implementation, event buses for local calls, factories of factories — structure added for problems nobody has, and it is not free.

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.

The question

How do I tell a design that is prepared for change from one that has been decorated with structure?

The requirement

A three-year-old service takes eleven files to add a field. Every layer has an interface, a factory, a mapper and a DTO, and every one has exactly one implementation. The team wants to know whether it is well designed or not.

The obvious build

It has interfaces, layers, dependency injection and patterns throughout. It follows the principles, so it is well designed and the eleven files are the price of that.

Why it breaks

The principles were means, not ends. Every one of them was justified by a change getting cheaper, and here adding a field — the most common change there is — costs eleven files. By the domain's own measure the design is failing (Changeability Is the Goal).

How it breaks as requirements change
  • The principles were means, not ends. Every one of them was justified by a change getting cheaper, and here adding a field — the most common change there is — costs eleven files. By the domain's own measure the design is failing (Changeability Is the Goal).
  • The abstractions have one implementation each, so none of them absorbs variation. They add hops and remove nothing (Interface Versus Implementation).
  • The mappers between layers are pure translation cost: a field must be added in the entity, the DTO, the request model, the response model and three mappers, and the compiler catches only some of it (The Anemic Domain Model).
  • Tests mirror the structure, so the test suite makes the structure expensive to change — the tests have become an argument for keeping the layers (Testing as Design Feedback).
  • Local reasoning is gone: answering "where does the price get set" traverses six files, none of which contains a decision (Local Reasoning).
RequirementConstraintsInvariantsResponsibilitiesBoundariesInterfacesStateDependenciesFailureImplementationTestsFeedbackEvolution

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.

Constraints
  • Nothing can be rewritten; the service is in production and owns revenue (The Risk in a Rewrite).
  • The engineers who built it are still on the team and believe it is a well-designed system, which it was intended to be (Tone, Disagreement and Receiving Review).
  • Every layer has tests, so removing a layer means removing its tests, which feels like reducing quality (Mocking).
Invariants
  • Any simplification must preserve behaviour exactly — this is refactoring, not redesign (What Refactoring Actually Is).
  • Nothing may be removed without evidence that no caller relies on it, which for internal abstractions is checkable (Characterization Tests).

Who owns what, and where the seams fall

Responsibilities decide boundaries; boundaries decide what an interface has to say.

Responsibilities
  • Someone must own the claim that a layer earns its keep, and be able to name the change it makes cheaper.
  • Review owns catching the first instance, because the eleventh is a cultural norm rather than a decision (Review as Design Feedback — and Why It Arrives Too Late).
  • The team owns the decision to remove structure, which is harder than adding it because it looks like reducing quality (Decision Records).
Boundaries
  • A layer is a boundary only if something is different on either side. A mapper that renames fields is not a boundary, it is a cost (Architecture Boundaries).
  • The test to apply to every seam: name a change that stops at this line. If nothing stops here, the line is decoration (Finding Seams).
  • Boundaries that genuinely earn their place — the vendor adapter, the transaction edge, the trust boundary — should survive any simplification pass untouched (Trust Boundaries).

The shapes, and what each one actually costs

Each row below is something a competent engineer added on purpose, in review, with a justification that sounded like this domain. The pattern across them is the same: a structure whose benefit is stated in the abstract and whose cost is paid on every change.

The response column is deliberately conservative. None of these is a reason to rewrite anything; each is a reason to stop adding the next one.

Structure with no named beneficiary
TriggerSymptomCauseResponse
An interface per classIThingService with exactly one implementer, foreverA convention absorbed as a principle, usually justified by testabilityRequire a named second implementation or a volatile dependency before an interface lands (Interface Versus Implementation).
An event bus for a local callemit(UserSaved) with one handler, two files away, in the same moduleCoupling treated as bad in itself rather than as a trade against traceabilityCall the function. Events are for consumers you do not know about (Observer).
Factory of factoriesAbstractHandlerFactoryProvider, three levels, one concrete pathAbstract Factory applied where a single factory — or a constructor — was the caseCollapse to the level that has a decision in it; usually that is one level, sometimes zero (Factory).
A mapper per layerEntity, DTO, request model and response model with the same six fieldsLayers introduced by template rather than because something differs across themKeep the mapping where a boundary is real — the wire, the vendor — and delete the internal ones (The Anemic Domain Model).
Config for something nobody configuresTwenty settings, nineteen at their defaults in every environmentFlexibility added speculatively, then frozen by fear of removing itDelete the ones with one value across all environments; they are constants with a deploy risk (Speculative Generality).
A repository wrapping an ORMfindById calling orm.findById, thirty times overA pattern copied without its condition — the ORM is already the abstractionKeep repositories that hold query knowledge; delete the ones that forward (When the Repository Is Just Indirection).

The tell: nothing in the file decides anything

There is one reliable diagnostic and it is not layer count. Open the file and look for a decision — a branch, a rule, a translation between genuinely different vocabularies, an invariant enforced. A file with none of those is a hop.

The fine case matters here as much as the diagnosis, because pass-through code is genuinely correct in a few situations and a team on a simplification campaign will delete those too (Tone, Disagreement and Receiving Review).

smellPass-through layer

looks like A class whose every method forwards to the same collaborator with the arguments unchanged, sometimes renaming a field on the way. Its tests assert that the forwarding happened.

suggests A boundary was drawn where nothing differs. It costs a file, a name, a hop, a set of tests and a place for the next engineer to add "just one small thing", and it makes every change touch one more file than it needs to (Change Amplification).

fix Inline it, delete its tests, and keep the behavioural tests one level out. Do it one layer at a time so any regression has an obvious cause, and record why it was removed so it is not reintroduced as an improvement (Architecture Decision Records).

when this is fine Genuinely correct in three cases. First, at a *published* boundary, where the indirection is a versioning seam that lets the inside change without breaking outside callers (API Stability). Second, where the forwarding crosses a trust or transaction boundary — the code looks identical and the meaning is not, because one side is untrusted or uncommitted (Trust Boundaries). Third, as the deliberate midpoint of an in-flight migration, where a temporary pass-through is what lets old and new coexist — provided it has an owner and an end date, because this is the case that quietly becomes permanent (Expand and Contract).

Price the same change twice

This is the argument that works, because it is a number rather than a preference. Pick the most ordinary change your system gets asked for, count what it touches today, and count what it would touch with three pass-through layers removed.

Note the direction of the signature device here: for once the *simpler* design is the "after", and the honest cost is not indirection but the day a real second implementation shows up and there is no seam waiting for it.

Add a nullable `costCentre` field, end to end
The change

Orders gain an optional cost-centre code, set at creation, shown in the API and in one report. No new behaviour, no new rule.

Layer per concern, interface per class, mapper per boundary
OrderEntityOrderDtoCreateOrderRequestOrderResponseOrderMapperOrderServiceImplIOrderServiceOrderRepositoryImplIOrderRepositoryOrderControllermigration
testsmapper_testservice_test (mocks the repo)repository_test (mocks the orm)controller_test (mocks the service)integration_test
11 modules · 5 test files

Eleven files, four of which only carry the field from one shape to an identical shape. Three of the five test files assert forwarding. None of the eleven contains a decision about cost centres.

One order module; boundaries kept only at the wire and the vendor
OrderOrderApi (request and response contract)orders/repositorymigration
testsorder_testapi_contract_testintegration_test
4 modules · 3 test files

Four files, each of which does something: the domain type, the published contract, the query, the schema. The API mapping stays because the wire format is a real boundary that must not track internal renames (Backward Compatibility as a Constraint).

what it cost The seams are gone, so the day a second persistence backend or a second consumer genuinely arrives, someone reintroduces a boundary under time pressure rather than finding one ready — a few days of work at the worst moment, and that risk is real rather than rhetorical. The simplified codebase also looks less like its sibling services, so an engineer moving between them loses the "every service is laid out the same" benefit that large organisations legitimately value. And the removal itself is a change to working revenue code with no user-visible improvement, funded from the same budget as features and defended in front of the people who wrote the layers (The Risk in a Rewrite).

How to build it

Most important first.

  • Inventory the abstractions and count implementations. Anything with one, that is not a test fake or a vendor seam, is a candidate for inlining (Speculative Generality).
  • For each layer, name the change it makes cheaper. Layers that cannot answer are removed one at a time, with the tests that only tested the layer (Extract Module).
  • Remove in the direction of the data flow and one layer per release, so a regression has an obvious cause (Incremental Migration).
  • Keep the seams with evidence behind them. This is not an argument against structure; it is an argument against structure with no named beneficiary (Over-Design and Under-Design).
  • Change the review norm at the same time, or the layers grow back. A rule that any new interface must name its second implementation or its volatile dependency is cheap and effective (A Review Checklist Worth Reading).
  • Write down what you removed and why, because the next engineer will otherwise re-add it as an improvement (Architecture 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.

Cost of the next change
  • This is the whole lesson: measure the current cost of a real change before arguing about the design. "Add a nullable field with a default" is a good probe because every system needs it and no design should make it expensive.
  • Eleven files for a field is a number, not an opinion, and it is the thing that turns a taste argument into an engineering one (Change Amplification).
  • After removing three pass-through layers, the same change costs four files: the entity, the migration, the API contract and one test. That number is the justification for the work (The Cost of Change).
  • What the simplification costs: the day a second implementation genuinely arrives, retrofitting the seam is real work — a few days, done under whatever pressure that day brings. That is the honest risk, and it is smaller than the certain cost of eleven files per change, but it is not zero (Reversible and Irreversible Decisions).
What the recommended approach costs
  • Removing structure is a change to working code with no user-visible benefit, funded from the same budget as features. It has to be justified with a number, and often cannot be.
  • A simpler design is genuinely worse on the day an unanticipated second implementation arrives, and that day sometimes comes.
  • The judgement required — which seams earn their keep — is harder than any rule, and a team without shared judgement will do this badly in one direction or the other.

What can go wrong

Failure modes
  • The over-correction: a team burned by layering removes real boundaries too, and the vendor SDK spreads into the domain within a quarter (Adapter).
  • Simplification stalls because a layer has tests, and deleting tests is politically hard even when they test nothing but a pass-through (Mocking).
  • The structure grows back, one reasonable interface at a time, because the norm was never discussed (Knowledge Sharing).
  • The diagnosis is used as a weapon in review, and the team stops proposing structure at all — including the structure that would have paid (Tone, Disagreement and Receiving Review).
Dependencies, and their direction
  • Each unnecessary layer adds a dependency edge in both directions — the caller on the layer, the layer on the next — so the dependency graph is denser than the design requires (Fan-in and Fan-out).
  • DI containers hide the density: a graph assembled by configuration looks flat and is not, which is why the cost is invisible until someone draws it (Service Locator).
  • Removing a layer removes two edges, which is why simplification passes tend to show a disproportionate improvement in build and test times.
Misreads
  • "So we should have no abstractions." No. The vendor adapter, the transaction boundary and the effect boundary earn their cost in almost every system. The claim is about abstractions with no named beneficiary (Adapter).
  • "Layers are the problem." Layers with a purpose are fine. The problem is a layer that translates without deciding, which is a cost with a diagram (Package by Layer).
  • "This is just YAGNI." YAGNI is about features. This is about structure added for variation nobody observed, which is a related but distinct failure and needs a different argument (YAGNI, With Its Bill Attached).
  • "The original team was incompetent." They followed advice that was widely taught, and the advice omitted the condition. Framing it as a skill failure guarantees the conversation goes nowhere (Tone, Disagreement and Receiving Review).
  • "We should have a rule: no more than three layers." Line-count rules and layer-count rules fail the same way. The question is whether each boundary contains a likely change, and no count answers it (Long Functions).
Smells this explains
  • utility-dumping-ground
  • shotgun-surgery
  • god-object

Testing it, and how it ages

What to test, and at which boundary
  • Characterize the current behaviour before removing anything; a pass-through layer that is not quite a pass-through is exactly the thing you will discover this way (Characterization Tests).
  • Delete tests that only assert a layer forwards a call. A test that fails only when the structure changes is a structural test, not a behavioural one (What a Unit Is).
  • After each removal, the same behavioural suite must pass unchanged. If it does not, you were not refactoring (The Refactoring Loop).
  • Track test-suite runtime as a secondary signal: pass-through layers usually come with pass-through tests, and both leave together.
How this design ages
  • Layers accumulate because adding one is always locally defensible and removing one is always locally frightening. Without a periodic pass this is a one-way ratchet (What Technical Debt Actually Is).
  • The strongest counter-pressure is a change-cost measurement repeated each quarter on the same probe change, so drift is visible before it is folklore (Revisit Triggers).
  • Codebases that survive this well tend to have one or two deliberate, defended boundaries and almost nothing else — which looks under-designed to a newcomer and is not (Designing a Monolith).

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.

  • GENERALStructure that does not contain a likely change is cost in any language and any paradigm; the specific shapes differ — layers and interfaces in OO, monad transformer stacks and needless typeclasses in functional codebases — but the failure is identical.
  • CONTESTEDThe strongest defence of the pattern canon and heavy layering: consistency has value independent of local optimality, because an engineer moving between twelve services in a large organisation can navigate any of them if they all look the same, and the "unnecessary" mapper is what lets one team change a database without another team noticing. Teams at that scale genuinely do report that uniform structure beats locally-minimal structure. The counter is that this argument is made most often by teams far below that scale, that it is unfalsifiable as usually stated, and that the canon itself — a 1994 catalogue of workarounds for missing language features — is a poor thing to be consistent about. Both halves deserve to be said out loud before anyone starts deleting layers (Patterns as Vocabulary).
  • SCALE-SPECIFICBelow about ten engineers, indirection with no named beneficiary is nearly pure cost. Above about a hundred, some indirection is buying independent deployability and team autonomy rather than change locality, and judging it by the file-count probe alone will reach the wrong answer.

Where the depth lives

This domain teaches the codebase-level structure and hands the rest off.

Domains that do not exist yet
  • Testing & Reliability Engineering — a test suite that mirrors the layer structure is what makes over-layering expensive to reverse, because removing a layer means deleting tests, and deleting tests reads as a reduction in quality however little they asserted.
  • System Design — the same failure at service grain, where an unnecessary layer is a network hop with its own deployment, on-call rota and failure mode rather than a file.