SOLID in 2026: which principles still save you and which ones just add files

Casey Holt

Casey Holt

August 27, 2026

SOLID in 2026: which principles still save you and which ones just add files

I have reviewed pull requests that added an interface, a factory, a decorator, and a folder named abstractions to change a string. The author said it was SOLID. The diff said it was fear. In 2026 I still use some of those letters. I do not use them as a permission slip to multiply types.

I led platform teams and I still read architecture for a living. This is the split I actually apply: which of the five still prevent a class of pain, and which mostly produce files you will rename in a month. I am leaving the “what if an agent wrote the first draft” argument for its own piece. Here the draft is human. The temptation is the same.

S: still saves you, if you stop at the seam

Single Responsibility is the one I would keep if I could only keep one. Not “a class does one thing” in the textbook sense. A module has one reason to change that you can say out loud. Billing changes when finance changes. The HTTP layer changes when the contract changes. If those two reasons live in one file, you will break checkout while fixing a header.

What does not save you is a file per verb. UserCreateService, UserUpdateService, UserDeleteService, each with an interface, each with a test that mocks the others. That is SRP as a filing system. I have walked into codebases like that. Nobody could find the invariant. Everyone could find a place to put another file.

The 2026 test: if you split this, does a future change touch fewer places that mean different things? If yes, split. If you split so the names look like a tutorial, you added files. Put the seam where the organization already has a seam — a team, a release, a failure domain — not where the acronym wants a noun.

Simple service folder on a laptop in morning light

O: sometimes saves you, often adds a hierarchy

Open/Closed is the letter I trust least as a default. “Open for extension, closed for modification” is a good description of a plugin boundary you already have: a payment provider, a rule engine, a lint rule. It is a bad description of a CRUD app that might grow a field.

I have seen OCP used to justify a strategy pattern for two branches. The if was readable. The strategies were a tour. When the third branch arrived, nobody extended anything. They modified the factory and two interfaces and the if came back in disguise.

Use OCP when modification is expensive: a published library, a stable core, a place where every edit is a compatibility event. Do not use it to avoid editing a file you own. Editing a file you own is the job. Extension points have a cost. You pay it whether anyone extends or not.

L: still saves you, because substitution is a lie people ship

Liskov is the quiet one and I still want it. If you pass a Square where a Rectangle was promised and the area is a surprise, you did not get polymorphism. You got a pun. The 2026 version is not shapes. It is a CachedRepository that does not write, a TestClock that lies about “now” in a way production never will, a subclass of a client that swallows errors because tests wanted green.

When a type says it is a drop-in, it has to honor the documented failure modes. If it cannot, it is not a subtype. It is a new type with a mapper. That mapper is a file I will accept. The pretend-subtype is a file I will revert.

Agents are very good at inventing subclasses that compile and fail the contract. You do not need an agent to do it. People have been doing it since the book. The letter still earns its keep because the bug is still popular.

I: the one that most often just adds files

Interface Segregation is correct in the small and a virus in the large. A client should not depend on methods it does not use. Fine. A client also should not depend on twelve one-method interfaces that exist so a mock can be generated. I have counted IUserReader, IUserWriter, IUserDeleter, IUserLister in a service that had one implementation and one test double. That is not segregation. That is a tax on grep.

The useful version is: do not make the HTTP handler depend on a god-interface that includes ExportPayroll because you stuffed everything into IUserService. Split when a consumer would have to stub a method it should never call. Do not split because a blog showed a diagram with four arrows.

In TypeScript and Go, small interfaces fall out of the language if you let them. You do not need a file per interface. You need a name at the use site. If the interface has one implementation and one consumer, it is often a lie you are telling for the test. Tell a simpler lie. Use a function.

D: saves you at the edge, wastes you in the middle

Dependency Inversion is why I can test a billing path without hitting Stripe. Invert at the edge: network, disk, clock, randomness, a vendor. Pass in a port. That port can be a function. It does not need a folder called domain and a folder called infrastructure if you have eight files total.

The waste is inverting everything so every new feels dirty. An internal helper that formats a date does not need an IDateFormatter injected through a container. A module that will only ever have one logger can import the logger. Purity about new is how you get a 400-line composition root for a script.

2026 has more generators that will happily emit the container wiring. That makes D cheaper to type and easier to over-apply. The letter still means “own your edges.” It does not mean “never import a concrete type you wrote.”

Reviewer with a notebook in front of a pull request

A week I still remember

We had a notifier that sent email, Slack, and SMS. Someone applied all five letters in one PR. Each channel got an interface, a factory, a decorator for retries, and a shared INotification that none of the channels quite fit. Liskov was already broken: SMS could not carry the HTML body the interface promised. Interface segregation “fixed” that with three more interfaces. Open/closed meant we did not edit the old switch; we edited the registry, the factory, and the tests that wired the registry. Shipping a fourth channel still took a day. The old switch would have taken an hour, and we would have seen the HTML lie on line 40.

The seam we actually needed was “format a message for a channel” as a function and “send bytes to a vendor” as an edge. Two ideas. Not a constellation. I reverted most of the PR and kept the vendor ports. That is D and a bit of S. The rest was a museum.

A filter I use in review

I ask three questions and I ignore the acronym until they are answered.

What change are we making cheaper? If the answer is “a future provider we do not have,” I want a sentence about when that provider arrives. If it is “never, but SOLID,” I want the extra types gone.

What change are we making more expensive? Every interface is a name people must learn. Every folder is a place a new hire will put the next mistake. If we made the happy path harder to read, we did not get design. We got ceremony.

Can I delete a file and keep the behavior? If yes, and the file exists to satisfy a letter, delete it. The principle that required it was not pulling its weight.

What I still teach

I still teach seams, substitution, and edges. I teach them with the incident that made them real: the time we changed a header and broke invoicing; the time a “fake” repository hid a write; the time we could not test refunds without a vendor sandbox. I do not teach a scavenger hunt for all five letters in every PR.

SOLID as a checklist produces files. SOLID as a memory of specific failures produces seams. In 2026 I have enough files. I do not have enough seams that match the way the system actually breaks.

If you want a default: keep S at organizational seams, keep L when you claim substitution, keep D at I/O, treat O as a privilege for stable boundaries, and treat I as a warning when the mock count exceeds the implementation count. That is not a poster. It is a bias that has survived contact with code I still have to change. The acronym can stay on the bookshelf. The seams have to stay in the repo.

More articles for you