When is it OK to break SOLID? A pragmatic guide
SOLID is a costs-down-the-line argument. When the down-the-line costs do not exist, the principle is dead weight.
S — Single Responsibility
Break it when: a class is genuinely cohesive but happens to have 3 methods of different "kinds". Splitting into three classes that pass the same data around makes things worse.
Real example — a Customer aggregate that owns identity, contact info, and preferences. Splitting into CustomerIdentity, CustomerContact, CustomerPreferences creates a fake distributed transaction every time you read a customer.
O — Open/Closed
Break it when: the abstraction is speculative. You can always extract an interface later when the second implementation actually arrives. Adding an interface "just in case" almost always costs.
L — Liskov Substitution
Break it when: you control all the callers and the "substitutability" guarantee was never claimed. A ReadOnlyList<T> that throws on Add violates LSP technically, but is a useful escape hatch — just do not type it as IList<T> then.
I — Interface Segregation
Break it when: the "wide" interface is the natural boundary of the abstraction. IServiceProvider has dozens of methods. Splitting it would explode the API surface for no gain.
D — Dependency Inversion
Break it when: the dependency is a stable primitive of the runtime — DateTime, Random, Encoding. Wrapping DateTime.UtcNow behind IClock is occasionally worth it for tests, but the cost is real (one more constructor parameter on every class).
The general escape hatch
// You can always inject a real method group as a delegate — no interface needed
public class TokenService(Func<DateTime> nowUtc) { ... }
// Tests pass () => new DateTime(2026, 1, 1);
// Prod passes () => DateTime.UtcNow;
What I look for in PR review
- Adding an interface with a single implementation and no test that uses it differently — usually delete the interface.
- Splitting a 300-line class into 5 50-line classes that always travel together — usually undo the split.
- Wrapping a stable library type ("StripeClient") behind an interface so you can mock it — fine, this is one of the cases the trade-off actually pays.
The honest rule
SOLID is a tax you pay to make change cheaper. Pay the tax where change is likely. Skip it where it is not.