SOLIDCONTESTEDPARADIGM-SPECIFICSCALE-SPECIFIC

How SOLID Gets Misused

An interface per class, layers that pass data unchanged, abstractions with one implementation — and the critiques of SOLID that are strong enough to deserve a straight answer.

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

This codebase followed the principles carefully and is miserable to change. What went wrong, and how much of it is the principles' fault?

The requirement

A three-year-old service: four hundred classes, three hundred and eighty interfaces, four layers, and adding an optional notes field to an order is estimated at two days. Every structure in it was added by a competent engineer citing a principle.

The obvious build

The codebase followed the principles, so the principles must be wrong; throw them out and go back to writing straightforward code. This is an understandable reaction to a genuinely bad codebase and it discards something valuable — the vocabulary for naming coupling problems — along with the checklist that caused the damage.

Why it breaks

Without the vocabulary the team loses the ability to name what is wrong. "This class is edited by three teams for unrelated reasons" is a SOLID-adjacent observation and it is true and useful (Divergent Change).

How it breaks as requirements change
  • Without the vocabulary the team loses the ability to name what is wrong. "This class is edited by three teams for unrelated reasons" is a SOLID-adjacent observation and it is true and useful (Divergent Change).
  • The reaction over-corrects: the next service has no seams at all, its domain imports the ORM, and its tests need a database — a different bad codebase, arrived at by rejecting the last one.
  • Blaming the principles hides the actual mechanism, which is that they were applied without a change-cost argument. The same failure happens with DDD, Clean Architecture and hexagonal, none of which is SOLID (Clean Architecture, and Where It Is Overused).
  • It also makes the cleanup unarguable. "These interfaces are bad because SOLID is bad" cannot be evaluated; "this interface has one implementation and keeps nothing off the classpath" can.
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
  • Nobody involved was careless. Every interface, layer and abstraction has a justification in a review comment, and most of the justifications name a letter.
  • The service works, is in production, and cannot be rewritten — the migration argument rules that out regardless of how the structure looks (The Risk in a Rewrite).
  • Two engineers on the team learned design from a SOLID-first curriculum and will experience "delete these interfaces" as an attack on their competence unless the reasoning is shared first.
  • Delivery pressure is real, so any remediation has to be incremental and has to pay for itself within a quarter.
Invariants
  • Behaviour must not change during any structural cleanup. A change that alters behaviour is not a refactor (What Refactoring Actually Is).
  • Every structure removed must have a stated reason it was not earning its cost, so the removal is a design argument rather than a matter of taste.

Who owns what, and where the seams fall

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

Responsibilities
  • The team owns the change-cost argument for every structure it keeps: which change does this make cheap, and has that change arrived?
  • A reviewer owns naming a concrete cost rather than a letter — this is the single highest-leverage habit change available here (Review as Design Feedback — and Why It Arrives Too Late).
  • The cleanup owns being incremental and reversible: delete the structures that are provably unused, leave the ambiguous ones, and revisit (Incremental Migration).
  • Nobody owns defending a structure on the grounds that a principle recommends it.
Boundaries
  • The boundary between a useful abstraction and ceremony is whether removing it would lose anything other than a hop. That question is answerable per structure, in a minute, and it is the entire cleanup method.
  • The boundary between the principles and their misuse is the presence of an observed problem. Every one of the five is sound when it is a response to something that happened and harmful when it is applied on sight (SOLID, Read Honestly).
  • A third boundary worth naming: published API versus internal code. Much of the advice that produces ceremony internally is correct for a library with unknown consumers (API Stability).

The four structures to look for

A misapplied-principles codebase has a recognisable inventory, and the useful thing about it is that each item is countable. You do not need a judgement to find three hundred and eighty interfaces with one implementation; you need a script and ten minutes.

Each row below names the structure, what produced it, and the specific question that decides whether this instance is earning its cost. The question matters more than the rule, because a small number of instances in every category will be legitimate.

What a principles-first codebase accumulates
TriggerSymptomCauseResponse
A class is createdAn interface is created alongside it, same name plus a prefix or suffix, one implementation, always changed in the same commit"Depend on abstractions" read as "every class needs an interface" — the forbidden form of DIPAsk what it keeps out. If it does not keep a vendor SDK off a package, is not implemented by a double the language could not otherwise create, and enforces no capability, delete it and rename the implementation (Dependency Inversion, Critically).
A layered architecture is adoptedA service method that calls one repository method and returns its result unchanged, plus a DTO identical to the entity and a mapper between themLayers treated as a structure to be filled rather than as boundaries with jobsAsk what the layer does at the boundary. Validating, adapting, protecting an invariant, or narrowing a contract all count; forwarding does not (Package by Layer).
A second case is anticipatedA strategy interface, a factory and a registry, with one strategy, three years laterOCP applied before the variation was observedAsk which second implementation exists. If the answer is hypothetical, inline it — re-extracting later is a keystroke and you will know the real shape (Speculative Generality, The Rule of Three).
A class looks like it does several thingsEleven classes, each one method, forming a call chain that must be read end to end to understand anythingSRP read as "one class, one thing", which has no stopping ruleAsk which stakeholder asks for changes to each. Units that always change together for the same requester belong together (Single Responsibility, Critically, Over-Decomposition).
The cleanup beginsA "no interfaces" rule, and six months later a domain package that imports the ORM and tests that need a databaseThe mitigation failing — a checklist replaced by its negation rather than by a questionKeep the ports around volatile dependencies and require a named cost for anything else, in both directions (Volatile Dependencies).

The layer that only forwards

Of the four, the pass-through layer is the most expensive and the easiest to defend, because it is always justified by a principle that sounds unarguable: separation of concerns. The test is what the layer *does* at the boundary. Translating a vocabulary, enforcing an invariant, narrowing a contract or protecting against an external model are all real jobs. Forwarding is not.

The cost is not the hop. It is that every additive change now touches every layer, so a field addition becomes eight edits, none of which contains a decision (Change Amplification).

smellPass-through layer

looks like OrderService.findById(id) { return this.repo.findById(id) }, plus an OrderDto with the same fields as Order and a mapper between them. Repeated across every entity in the system, and often with an interface in front of each.

suggests A layer added because the architecture diagram has one, rather than because something happens at that boundary. It multiplies the edit count of every additive change by the number of layers while adding no decision, no protection and no translation (Package by Layer).

fix Delete the layer for the entities where it forwards, and keep it where something happens. Uniformity is not worth eight edits per field: a codebase where three entities have a service layer because they need one, and twelve do not, is more honest than one where all fifteen have an empty one. If the DTO is a published contract, keep it and write down why, so the next reader does not delete it (Decision Records).

when this is fine Genuinely correct in several cases, and they are not rare. A DTO that is deliberately identical *today* but is a published API contract that must not follow the entity when it changes is doing real work — the duplication is the point (Backward Compatibility as a Constraint). An anti-corruption layer that currently maps one-to-one but exists to absorb an external model's future churn is a bet, and a reasonable one where the external party is genuinely volatile (Anti-Corruption Layer). A thin service method that will shortly hold a transaction boundary or an authorization check is a placeholder with a named plan. And in a codebase where the alternative is controllers reaching into repositories directly, a uniform thin layer can be cheaper than the inconsistency it prevents.

The critiques, taken seriously

SIMULATEDThe class, interface and layer counts in this lesson's scenario are illustrative figures from an Engineer Atlas model of a mid-sized service, not a measurement of a real codebase. The *shape* is what transfers and it transfers reliably — an interface count close to the class count, an additive change touching every layer — while the specific numbers would tell you nothing about your own system. Count yours; it takes ten minutes and the result is evidence rather than an anecdote.

This module has argued throughout that the five principles are heuristics rather than laws. It is worth finishing by stating the case against them at full strength, because a lesson that cannot do that has not understood the argument — and because the critics include people with more large-system experience than most advocates.

The right-hand column is where this lesson's own position lives, and it is deliberately narrow. Each critique lands; what survives is smaller than what is usually taught and larger than nothing.

The critiqueWhat it gets rightWhat survives it
Too vague to be actionable — "one reason to change" can justify splitting a class or keeping it whole, depending on framingCorrect for SRP and largely for OCP. Two competent engineers can cite the same principle for opposite designs, neither misapplying it, which means the principle is not deciding anything — the framing isThe observable version: several stakeholders have repeatedly edited this unit for unrelated reasons. That is history, not framing, and it is worth acting on (Divergent Change)
No empirical support — no controlled evidence that SOLID codebases have fewer defects or cheaper changesCorrect, and rarely acknowledged. There is a real literature on coupling and cohesion; there is essentially nothing evaluating these five as such, and advocacy rests on argument and experienceArgument and experience are legitimate grounds for a heuristic. They are not grounds for a rule, a checklist or a review gate — which is exactly the distinction this module is drawing
Formulated for 1990s OO — inheritance-centric, expensive recompilation, shipped binariesCorrect, and it explains a lot. Meyer's OCP addressed a distribution problem that continuous deployment removed; ISP addressed C++ build times that modern toolchains largely removedThe coupling phenomena outlived their original mechanisms. Blast radius on a shared edit is real whether or not anything is recompiled (Change Amplification)
Transfers badly to other paradigms — functional, data-oriented and ECS designs get little from themCorrect. In FP, SRP is what a function is, DIP is a parameter, and importing the class ceremony produces worse code than writing none of itThe concerns transfer; the mechanisms do not. Stating them paradigm-neutrally — who edits this, what is the blast radius, can callers rely on this — keeps the useful part (SOLID, Read Honestly)
Produces the codebase in this lesson — ceremony, indirection, eight edits for a fieldCorrect, and it is the most common outcome of teaching them as a checklist. This is not an unlucky misapplication; it is the predictable result of a rule with no stopping conditionEvery one of the five is sound as a response to an observed problem and harmful as a policy. That single distinction accounts for most of the damage (The Cost of Change)

How to build it

Most important first.

  • Inventory before acting. Count interfaces with exactly one implementation and no test double, layers whose methods only forward, and abstractions no second case ever justified. The counts are usually shocking and they are evidence rather than opinion.
  • Delete in the safest order: pass-through layers first (they have no behaviour), then one-implementation interfaces that keep nothing off the classpath, then speculative extension points (Speculative Generality).
  • Keep everything around a genuinely volatile dependency. The cleanup should end with a small number of ports that all have a reason (Volatile Dependencies).
  • Replace the review vocabulary: name the symptom, not the letter. This is what stops the codebase regrowing the same structure in six months (A Review Checklist Worth Reading).
  • Do it as a series of small, behaviour-preserving refactors with the tests you have, not as a project. A structural cleanup presented as a rewrite will be cancelled at the first delivery conflict (The Refactoring Loop).

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
  • Before: adding an optional notes field costs eight edits — entity, DTO, request model, mapper, interface, implementation, view model, and a test double — none of which contains a decision. Two days, and the estimate is accurate.
  • After removing pass-through layers and single-implementation interfaces: the same field costs two edits and a test. The change is additive and the design is finally proportional to it (Change Amplification).
  • The change that gets *harder*: any that genuinely needs a seam where one was deleted. This is why the cleanup keeps the ports around volatile dependencies and touches nothing else.
  • The most valuable change in cost is the invisible one: engineers stop routing around the structure. A codebase where the cheap thing and the right thing coincide gets better on its own (Evolvability).
What the recommended approach costs
  • Removing structure is risk with no feature attached. It competes with roadmap work and usually loses, which is why it should be done in small pieces alongside feature work rather than as a project.
  • The team loses some genuine optionality: a few of the deleted interfaces would have been useful eventually, and re-extracting costs a refactor under whatever pressure exists then.
  • "Name the cost, not the letter" makes reviews slower and occasionally blocks a change that was fine, and it asks more of junior engineers than a checklist does.

What can go wrong

Failure modes
  • The cleanup deletes an interface that was keeping a vendor SDK off the domain classpath, and the boundary regresses in the name of simplicity.
  • The cleanup becomes its own ideology — "no interfaces" — which is the same mistake with the sign flipped.
  • It is scoped as a project, takes a quarter, and is cancelled at eighty percent, leaving a codebase with two conventions (Incremental Migration).
  • The mitigation fails socially: the engineers who built the structure experience its removal as a judgement, and the team stops surfacing design disagreements at all (Tone, Disagreement and Receiving Review).
Dependencies, and their direction
  • Every deleted interface removes a dependency edge and a file. The dependency it "managed" was almost always to a type in the same module.
  • Every deleted layer removes a translation step and a data type that existed to be mapped to another data type.
  • What remains depends on the same things it always did, more visibly and with fewer hops (Local Reasoning).
Misreads
  • "So SOLID is bad." The principles name real phenomena and the vocabulary is worth keeping. What is bad is applying any structural heuristic without an observed problem and a change-cost argument — and the same failure is available with DDD, Clean Architecture, microservices and every pattern catalogue (Pattern Overuse).
  • "Delete all the interfaces." Delete the ones with one implementation that keep nothing off the classpath and enforce no capability. The ones around volatile dependencies are the reason the domain is testable at all (Volatile Dependencies).
  • "More layers means better separation." It means more translation and more places to edit. A layer earns its place by doing something at the boundary — validating, adapting, protecting — and a layer that forwards is a cost with no counterpart (Boundary Adapters).
  • "The engineers who built this were bad." They were following the most widely taught design advice in the profession, carefully. That is precisely what makes this failure worth a lesson rather than a complaint (Tone, Disagreement and Receiving Review).
Smells this explains
  • speculative-generality
  • utility-dumping-ground
  • shotgun-surgery

Testing it, and how it ages

What to test, and at which boundary
  • Characterization tests before touching anything, because the structure being removed is load-bearing until proven otherwise (Characterization Tests).
  • After deleting a layer, the tests that only asserted forwarding should be deleted with it. A test that mirrors structure has no independent value (Mocking).
  • Keep and strengthen the tests at the remaining real boundaries — the ports around volatile dependencies (Contract Tests).
  • Measure the cleanup with a change-cost test, not a structure count: take a real recent change and re-price it (Change Amplification).
How this design ages
  • Ceremony regrows unless the review vocabulary changes. Deleting three hundred interfaces without changing what a review comment says buys about eighteen months.
  • The structures that survive a cleanup tend to be the right ones, because they are the ones somebody could defend with a concrete cost. That is a reasonable ongoing filter.
  • As the system genuinely grows, some deleted seams will be needed again, and re-extracting them at that point is a mechanical refactor informed by two real cases rather than a guess (The Rule of Three).

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.

  • CONTESTEDThe critiques of SOLID deserve to be stated at full strength, because they are serious and largely correct. (1) Several of the principles are vague enough to justify opposite designs: "one reason to change" can be used to argue for splitting a class and for keeping it whole, depending on how the speaker frames "reason", which means the principle is doing no work — the framing is. Two competent engineers can cite SRP at each other and neither is misapplying it. (2) The empirical support is thin. There is a real literature on coupling and cohesion metrics; there is almost nothing that evaluates these five principles as such, and no controlled evidence that SOLID codebases have fewer defects or cheaper changes. Advocacy for them rests on argument and experience, which is legitimate but should not be described as evidence. (3) They were formulated for 1990s class-based OO with expensive recompilation and shipped binaries, and they transfer unevenly: in functional, data-oriented and ECS designs, several are either automatic, meaningless, or actively misleading, and the ceremony they carry travels better than the ideas do. Against all of that, the case for keeping them is narrow and, this lesson argues, still worth something: the phenomena they point at — divergent change, blast radius, broken substitutability, fat contracts, untestable policy — are real, common, and hard to discuss without vocabulary. A team that discards the vocabulary usually does not replace it with something better; it replaces it with nothing.
  • PARADIGM-SPECIFICThis particular failure mode — interface per class, pass-through layers, single-implementation abstractions — is characteristic of class-based OO codebases with a container. The functional equivalent exists and looks different: excessive type-class abstraction, monad transformer stacks assembled for generality nobody uses, and point-free code written for elegance rather than for a reader. The underlying error is the same in both, which is a good sign that it is not really SOLID's fault: it is structure adopted without an observed problem (Premature Abstraction).
  • SCALE-SPECIFICAt four hundred classes and one team, this structure is pure overhead and can be deleted quickly. At forty thousand classes across twenty teams, some of the same structure is load-bearing coordination — an interface is how two teams agree without a meeting — and deleting it on the same reasoning would be a serious mistake. The cleanup argument here is calibrated to the first case.

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 a pass-through layer is the clearest evidence that the layer has no behaviour, and deleting those tests alongside the layer is part of the cleanup.
  • System Design — the same failure at service granularity is a set of microservices that forward requests to each other without deciding anything, and it is much more expensive there because the hops are network calls.
  • Programming Languages & Runtime Internals — how much ceremony a boundary costs, and whether the compiler can remove it, differs enough between languages that the same structure is negligible in one and a real tax in another.