SOLID Principles Explained with Examples: When They Help and When They Become Dogma
All five SOLID principles with real code examples, explained without fluff — and with honesty about when they produce overengineering instead of solving problems.
You've seen codebases where every database operation lives in its own class, every formatter has its own interface, and tracing what saveUser() actually does requires navigating seven files. That code was probably written by someone who just discovered SOLID.
The SOLID principles have real value — when applied with judgment. The problem is that they're taught as absolute rules and applied as checklists, producing exactly the kind of overengineering they were supposed to prevent.
What SOLID is and where it came from
SOLID is an acronym for five object-oriented design principles formulated by Robert C. Martin in the early 2000s. If you've read the post on Clean Code without dogma, you've seen these principles mentioned in passing — here we go through each one with real code and the question that matters: when does this principle help, and when does it become bureaucracy?
The five are:
- S — Single Responsibility Principle
- O — Open/Closed Principle
- L — Liskov Substitution Principle
- I — Interface Segregation Principle
- D — Dependency Inversion Principle
S — Single Responsibility Principle
A module should have one, and only one, reason to change.
# Bad: User does everything
class User:
def save(self): ...
def send_welcome_email(self): ...
def generate_report(self): ...
# Better: each class has one responsibility
class UserRepository:
def save(self, user): ...
class UserNotifier:
def send_welcome_email(self, user): ...
SRP is the most useful of the five — and the most misunderstood. "One responsibility" doesn't mean "one method." A class OrderProcessor that validates, calculates taxes, and persists an order may be fulfilling a single responsibility: processing orders. What you don't want is OrderProcessor sending marketing emails or generating PDF reports — that's a different responsibility, with a different reason to change.
When it becomes dogma: when you split a cohesive 30-line function into three 10-line functions that only make sense together, because "each function should do one thing." SRP is about cohesion of purpose, not line count.
O — Open/Closed Principle
Software should be open for extension and closed for modification.
# Bad: every new format requires modifying the class
class ReportExporter:
def export(self, data, format):
if format == "pdf":
...
elif format == "csv":
...
# adding "xlsx" means touching this
# Better: new formats are extensions
class ReportExporter:
def export(self, data, formatter: ReportFormatter):
return formatter.format(data)
class PdfFormatter(ReportFormatter): ...
class CsvFormatter(ReportFormatter): ...
OCP is powerful when you have a real extension point — a plugin system, export formats, payment providers. The example above makes sense if you genuinely add new formatters regularly.
When it becomes dogma: when you create abstractions for extensions that will never happen. "What if we need another format in the future?" is the question that turns simple code into class hierarchies nobody asked for. YAGNI still applies.
L — Liskov Substitution Principle
Subclasses must be substitutable for their superclasses without breaking expected behavior.
# Classic violation: Square inherits Rectangle but breaks invariants
class Rectangle:
def set_width(self, w): self.width = w
def set_height(self, h): self.height = h
def area(self): return self.width * self.height
class Square(Rectangle):
def set_width(self, w):
self.width = w
self.height = w # breaks Rectangle's behavior contract
If you have code that works with Rectangle and you pass a Square, it will produce unexpected results. LSP says that inheritance should preserve the parent class's contract — not just the interface, but the expected behavior.
When it becomes dogma: LSP presupposes inheritance. In languages with duck typing (Python, JavaScript, Go) or in predominantly functional code, LSP rarely has direct application. Creating class hierarchies just to say that LSP is being respected is bureaucracy without benefit.
I — Interface Segregation Principle
Don't force a class to implement methods it doesn't use.
// Bad: a fat interface
interface Worker {
work(): void;
eat(): void;
sleep(): void;
}
// Better: focused interfaces
interface Workable { work(): void; }
interface Feedable { eat(): void; }
class Robot implements Workable {
work() { ... }
// doesn't need to implement eat() and sleep()
}
ISP is useful when you have implementations that need subsets of functionality. A Robot that implements Worker and is forced to have a eat() method that does nothing (or throws) is a design smell.
When it becomes dogma: when you create one interface per individual method because "interfaces should be small." The example above with Workable and Feedable is reasonable. A Saveable interface with a single save() method, implemented by exactly one class and used in exactly one place, is overhead without purpose.
D — Dependency Inversion Principle
High-level modules should not depend on low-level modules. Both should depend on abstractions.
# Bad: UserService depends directly on PostgreSQL
class UserService:
def __init__(self):
self.db = PostgreSQLConnection() # concrete coupling
def find_user(self, id):
return self.db.query(f"SELECT * FROM users WHERE id={id}")
# Better: depends on the abstraction
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo
def find_user(self, id):
return self.repo.find(id)
This is the highest-ROI principle of the five. When UserService depends on UserRepository (abstraction), you can inject PostgreSQLUserRepository in production and InMemoryUserRepository in tests. Without DIP, testing business logic requires a running database — which is exactly the kind of friction that kills test coverage over time.
When it becomes dogma: when you create interfaces with a single implementor that will never change. If EmailService has an IEmailService interface implemented only by SMTPEmailService and you'll never swap SMTP for another mechanism, the interface exists solely to say you "applied DIP." The real benefit of DIP is substitutability — if there's no real substitution, there's only real overhead.
The pattern across all five
If you noticed, all five have the same structure: they make sense when there's real variability that justifies the abstraction. Variable export formats — OCP applies. Interchangeable database implementations — DIP applies. Geometric shape hierarchies — LSP applies.
The mistake isn't applying SOLID. The mistake is applying SOLID preemptively, before the variability exists. Code with unnecessary abstractions is harder to read and modify than direct code — exactly the opposite of the goal.
Martin Fowler has a good line on this: premature abstractions are as harmful as premature optimizations. You're paying the cost of indirection without getting the benefit of flexibility.
SOLID and Clean Code: the actual relationship
SOLID and Clean Code aren't the same set of ideas — they're complementary with different focuses. Clean Code deals primarily with readability at the line and function level: naming, size, comments. SOLID deals with design at the class and module level: responsibilities, dependencies, extensibility.
You can have Clean Code that violates SOLID (a well-named 15-line function that does too many things) and SOLID code that violates Clean Code (a correctly abstracted hierarchy with terrible names and no comments explaining the design).
The real overlap is in SRP — and in DIP when it's at the function level via dependency injection. The rest operate on different layers of analysis.
When comparing a before-and-after of applying one of these principles — especially when explaining a design decision to someone else — I use the Code Diff Checker to put both versions side by side. Faster than going back and forth in Git history when you're walking through a refactor.
Frequently asked questions
Does SOLID still apply to functional languages or modern TypeScript?
Partially. SRP and DIP have direct equivalents in functional code — module cohesion and dependency injection via parameter. OCP maps to function composition. LSP and ISP are less relevant because class inheritance is less central. In modern TypeScript with composition and structural types, you often apply the principles without the class hierarchies that classic examples use.
Which of the five principles has the highest practical impact?
DIP, without question. The ability to substitute implementations — especially for testing — has direct returns on coverage and development speed. SRP comes second, but at the module/service level, not the function level. OCP, LSP, and ISP depend heavily on you actually having the variation points that justify the abstractions.
Can you apply all five at once without overengineering?
Yes, but only when all five have concrete justification in the same design. A data repository naturally calls for DIP (database abstraction), ISP (separate interfaces for reading and writing), and SRP (the repository doesn't do business validation). When the five overlap organically, it's a sign the design has real variability. When you have to force one of them, it's a sign it doesn't belong there.
Does SOLID only apply to OOP?
It was formulated in an OOP context, but the principles have analogs in other paradigms. In Go, for example, small interfaces and composition reflect ISP and DIP. In functional Python, cohesive modules with dependency injection via parameter reflect SRP and DIP. The name and classic examples are OOP — the underlying principle is broader.
SOLID is a map, not a destination
The five principles describe symptoms of real design problems: high coupling, rigidity, fragility, untestability. When you identify those symptoms in code, SOLID offers directions to resolve them.
Applying SOLID preemptively — before the symptoms appear — is like taking medicine for a disease you don't have yet. The risk is that the medication has side effects: unnecessary complexity, indirection without benefit, reduced readability.
The useful version of SOLID is: know the principles, recognize the symptoms each one solves, apply when the symptoms appear. That's the difference between an engineer who uses SOLID and one who follows SOLID.
- 01 Clean Code Without Dogma: What Actually Matters Clean Code became a religion. Here's which principles have real ROI, which are cargo-culted rules, and how to push back on dogmatic code reviews.
- 02 IP Addresses: IPv4 vs IPv6, exhaustion, NAT, and why it still matters IPv4 ran out in Latin America in 2020. A practical breakdown of notation, NAT's tradeoffs, IPv6 changes, and what it means for developers writing real systems.