SOLID Principles in .NET: What They Mean and Why They Matter
SOLID is five design heuristics for object-oriented code that must change safely. Robert Martin popularized the acronym; the ideas are older than the name. They are not laws of physics — they are vocabulary for recognizing when a class has too many reasons to change, when extension requires editing tested code, or when a subtype silently breaks a contract.
This guide treats SOLID as a knowledge map: each letter is a precise claim, a common violation, and a C# fix you would see in a backend API. We also place SOLID on top of OOP pillars and under Clean Architecture layers — because principles without a place in the dependency graph become slogans. Benefits like “easy testing” and “low coupling” are consequences, not a sixth principle.
See how these dependency rules scale into Clean Architecture in .NET. For a broader toolbox around service boundaries, compare the microservices design patterns.
SOLID as a knowledge map
Read the five letters as related constraints, not a checklist to tattoo on every class. SRP limits why a module changes. OCP limits how you add behavior without rewriting what already works. LSP and ISP protect the honesty of type hierarchies and interfaces. DIP flips dependency arrows so policy (business rules) does not import volatile details (SQL, SMTP, Stripe).
A useful philosophy: optimize for changeability under incomplete foresight. You cannot predict every feature, but you can refuse designs that make the next feature touch five unrelated teams’ code. Premature abstraction is also a cost — apply a principle when you feel the pain it names, not because an infographic listed five icons.
Quick reference
- Definition: SOLID = five OO heuristics for safe change — not a framework, not Clean Architecture itself.
- Consequence ≠ principle: clean tests and low coupling follow from applying the five; they are not substitutes for them.
- Common error: treating SOLID as mandatory on every DTO and one-liner helper.
- Heuristic: if adding a feature forces edits in many unrelated modules, name which letter you violated.
- Interview angle: explain one principle with a violation and a fix — slogans without mechanism fail design reviews.
Remember this
Applying a principle before feeling the pain it names is premature abstraction, not discipline — a feature that forces edits across five unrelated modules names exactly which letter got violated.
Single Responsibility Principle
A class should have one reason to change — not “one method,” not “one field.” If two different stakeholders could request changes for different reasons, the class is overloaded. An OrderService that places orders, sends email, writes invoices, and adjusts inventory changes when business, notifications, finance, or warehouse rules change. That is four axes of change in one type.
The mechanism: split along the axis of change, then reconnect with events or orchestration. The philosophy is cohesion under ownership — not microscopic classes for vanity. A pure orchestrator that only sequences steps still has one responsibility: orchestration.
Quick reference
- Ask: 'What would make me change this class?' Multiple different answers = multiple responsibilities.
- A class with more than ~200 lines is a warning sign — not a rule, but worth checking.
- SRP at the module level too: a namespace/folder should have one reason to exist.
- Domain events (OrderPlacedEvent) are a great way to separate the core action from side effects.
- Services that 'just orchestrate' are fine — the responsibility is orchestration, not implementation.
Remember this
Group behavior by who has reason to change it, not by what data it touches. Use domain events to separate core business actions from notification and reporting side effects.
Open/Closed Principle
Software entities should be open for extension and closed for modification: new behavior should not require editing already-tested code. The classic violation is a growing switch on a type tag — every new case risks every old case.
Mechanism: introduce a stable abstraction (strategy, plugin, pipeline step) and add variants as new types. Truth check: OCP is not “never edit files.” Bug fixes and API redesigns still touch code. OCP targets the predicted dimension of variation (discount kinds, payment providers). Abstracting everything “just in case” is a different — and expensive — mistake.
Quick reference
- OCP is usually achieved through the Strategy pattern (swappable algorithms) or Template Method (fixed skeleton, swappable steps).
- In .NET: interfaces, abstract classes, and DI container registration enable OCP at the service level.
- Use OCP when you can predict the dimension of change. Not every class needs to be open — premature abstraction is a real cost.
- The Decorator pattern extends behavior without modifying the original: wrap IEmailSender to add logging, retry, rate limiting.
- Configuration-driven extension: adding a new discount via config (not code) is a form of OCP.
Remember this
Replace switch-on-type with polymorphism. Define an interface for the variation point, implement a class per variant, dispatch through the type system.
Liskov Substitution Principle
If S is a subtype of T, objects of type S must be usable wherever T is expected without breaking the program’s correctness. Subtypes must keep the promises of the base contract: no stronger preconditions, no weaker postconditions, no surprise exceptions for methods the base said were safe.
A ReadOnlyRepository that implements IRepository and throws on Add is a textbook violation — callers that trust IRepository are lying to themselves. Mechanism: narrow the type (ISP) so you never advertise capabilities you cannot honor. Symptom: is / as checks scattered through polymorphic code.
Quick reference
- LSP is often violated by throwing NotSupportedException in overrides — a code smell.
- Check your overrides: do they strengthen preconditions (require more than base)? That's a violation.
- Do they weaken postconditions (promise less than base)? That's a violation.
- Symptom: instanceof / is checks in polymorphic code. The caller shouldn't need to know the concrete type.
- Interface segregation (next principle) often solves LSP violations by splitting over-broad contracts.
Remember this
Subtypes must honor the contracts of their base type. If a subtype can't fulfill a method, the interface is too broad — split it.
Interface Segregation Principle
Clients should not be forced to depend on interfaces they do not use. A fat interface forces every implementer to stub irrelevant methods — often with NotSupportedException, which is also an LSP failure. Mechanism: split by caller role (reader, writer, authenticator), not by “everything this aggregate can do.”
Philosophy: an interface is a promise to a client, not a dump of a class’s public surface. Query handlers should not compile against write APIs they never call — that coupling slows change and bloated mocks.
Quick reference
- A good interface is small enough that every implementer implements every method without stubs.
- ISP complements SRP: SRP keeps classes focused, ISP keeps interfaces focused.
- In .NET with DI, narrow interfaces make testing trivial — mock only the methods the class under test calls.
- Role interfaces: define interfaces based on the role a class plays for its client, not on what it is.
- Marker interfaces (IDisposable, ICloneable) are legitimate — they express a capability, not a method set.
Remember this
Define interfaces from the caller's perspective. If an implementer has to throw NotImplementedException, the interface is too fat — split it.
Dependency Inversion Principle
High-level modules should not depend on low-level modules; both should depend on abstractions. The inversion is ownership: OrderService (policy) defines IPayment / IOrderRepository; CreditCardPayment and EfOrderRepository (details) implement them. Dependency arrows point inward toward policy — not from business code into SQL drivers.
Infographics often show Order → Payment <<interface>> → CreditCard | PayPal. That picture is right when the interface is stable and owned by the use case. It is wrong when “use an interface” means a low-level MySqlRepository type still imported directly by Order — that is decoration, not inversion. DIP is what makes constructor injection meaningful; DI without ports is just passing objects around.
Quick reference
- DIP is what makes dependency injection meaningful — DI without interfaces is just passing objects around.
- The interface belongs to the high-level module, not the low-level one. This is the 'inversion' in the name.
- AddScoped: one instance per HTTP request. AddTransient: new instance every time. AddSingleton: one for the app lifetime.
- Constructor injection is the standard in .NET — inject via constructor, not property or method.
- Test doubles: use FakeXxx for in-memory implementations in unit tests. Use Mock<IXxx> for verifying interactions.
- DIP enables the Clean Architecture layers: Application defines interfaces, Infrastructure implements them.
Remember this
High-level business modules define interfaces; low-level infrastructure implements them. Inject via constructor. This is what makes your business logic testable without a database.
OOP pillars under SOLID
SOLID assumes classical OOP machinery. A class is a blueprint; an object is an instance with state and behavior. Encapsulation keeps invariants inside the type so callers cannot corrupt them. Inheritance and polymorphism let you vary behavior behind a shared contract. Abstraction hides irrelevant detail so callers program to roles, not storage engines.
Truth: SOLID does not replace these pillars — it disciplines them. Inheritance without LSP becomes a trap. Interfaces without ISP become fat lies. Polymorphism without OCP collapses into switches. If your language style is more functional, map the same ideas (cohesion, extension points, honest contracts, dependency direction) without forcing class hierarchies.
Quick reference
- Class/object: shared shape, many instances — not a substitute for modules.
- Encapsulation: public contract vs private representation; protects invariants.
- Inheritance: reuse and subtype relationships — dangerous without LSP.
- Polymorphism: same call site, different behavior — enables OCP.
- Abstraction: essential vs accidental detail — feeds DIP ports.
- When OO is wrong for the problem, do not force SOLID theater — keep the heuristics.
Remember this
Inheritance without LSP becomes a trap, interfaces without ISP become fat lies — SOLID disciplines the OOP pillars, and forcing class hierarchies where the language style is functional keeps the theater without the benefit.
Layers: where SOLID meets Clean Architecture
Infographics stack Presentation → Application → Domain → Infrastructure. That stack is a dependency rule, not magic boxes: outer layers may know inner ones; domain must not import EF, HTTP, or SMTP. DIP lives naturally here — Application defines ports; Infrastructure adapters implement them; Presentation only talks to Application use cases.
SOLID without layers still helps a single project. Layers without SOLID still tangle use cases with SQL. Together: SRP keeps handlers focused, OCP/DIP let you swap payment and persistence, ISP keeps ports small, LSP keeps fakes honest in tests. For the full .NET walkthrough of projects and CQRS, see the Clean Architecture guide — this section is the map, not a second copy of that article.
Quick reference
- Presentation: UI/API entry — depends on Application, not Domain details.
- Application: use cases + ports (interfaces) — depends on Domain.
- Domain: entities and rules — zero framework dependencies.
- Infrastructure: DB, email, gateways — implements Application ports.
- Product default ≠ theorem: some apps collapse layers until pain appears — that is a trade-off, not a free pass to cyclic spaghetti.
- Practice: draw arrows for one feature; any arrow from Domain to Infrastructure fails the rule.
Remember this
Any arrow from Domain to Infrastructure fails the dependency rule — layers without SOLID still tangle use cases with SQL, and SOLID without layers still helps but only inside a single project.
When the pain names the principle
Do not apply all five to every file. Match the symptom: many reasons to change → SRP; must edit a switch to add a variant → OCP; subtypes throw or callers type-check → LSP/ISP; tests need a real database for a business rule → DIP.
Philosophy for reviews: ask what property you are protecting (cohesion, extension safety, contract honesty, testability). If nobody can state the property, the SOLID citation is theater.
Quick reference
- Pain → principle is more reliable than principle → refactor everything.
- Over-abstraction has a cost — YAGNI still applies.
- Prefer one real port and one fake over five empty interfaces.
- Document the variation point you opened (payments, discounts) so the team knows what OCP was for.
Remember this
Pain naming the principle beats principle-first refactoring — one real port and one fake beats five empty interfaces, and a SOLID citation nobody can tie to a protected property is theater.
Key takeaway
SOLID is a compact language for safe change in object-oriented systems: one reason to change, extend without rewrite, honest subtypes and interfaces, and dependencies that point toward policy. The payoff — cleaner tests, lower coupling, maintainability — follows when the principles are applied where pain appears, not where an acronym wants stickers.
Practice (25 min): Take one “god” service in a sample API. List its reasons to change (SRP). Extract one side effect behind an interface you own (DIP). Add a second fake implementation for a unit test. If you cannot name which letter you applied, the refactor was random.
Related Articles
Explore this topic