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.
This codebase followed the principles carefully and is miserable to change. What went wrong, and how much of it is the principles' fault?
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 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.
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).
- 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.
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.
- 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.
- 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.
- 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.
- 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.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| A class is created | An 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 DIP | Ask 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 adopted | A service method that calls one repository method and returns its result unchanged, plus a DTO identical to the entity and a mapper between them | Layers treated as a structure to be filled rather than as boundaries with jobs | Ask 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 anticipated | A strategy interface, a factory and a registry, with one strategy, three years later | OCP applied before the variation was observed | Ask 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 things | Eleven classes, each one method, forming a call chain that must be read end to end to understand anything | SRP read as "one class, one thing", which has no stopping rule | Ask 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 begins | A "no interfaces" rule, and six months later a domain package that imports the ORM and tests that need a database | The mitigation failing — a checklist replaced by its negation rather than by a question | Keep 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).
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).
The critiques, taken seriously
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 critique | What it gets right | What survives it |
|---|---|---|
| Too vague to be actionable — "one reason to change" can justify splitting a class or keeping it whole, depending on framing | Correct 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 is | The 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 changes | Correct, 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 experience | Argument 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 binaries | Correct, 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 removed | The 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 them | Correct. In FP, SRP is what a function is, DIP is a parameter, and importing the class ceremony produces worse code than writing none of it | The 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 field | Correct, 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 condition | Every 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.
- Before: adding an optional
notesfield 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).
- 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
- 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).
- 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).
- "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).
- speculative-generality
- utility-dumping-ground
- shotgun-surgery
Testing it, and how it ages
- 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).
- 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.
- — 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.