Why this matters
Every senior engineer carries a scar from a class called something like Manager,
Utils, or God. Ten thousand lines. Touched by forty people. Nobody dares delete a
line, because nobody knows which three features that line is secretly holding up. You
change the tax calculation and the email templates break. You fix the emails and login
breaks. The code works — right up until you need it to do anything new.
SOLID is five principles for never building that class in the first place. The principles come from Robert C. Martin's writing on object-oriented design — first collected in Agile Software Development (2002); the SOLID acronym itself was coined a couple of years later by Michael Feathers, who noticed the initials spelled a handy word. Martin revisited them in Clean Architecture (2017). You've probably seen the acronym before. This lesson is about what the letters actually buy you — not as rules to memorize, but as instincts for splitting code into pieces that stay easy to change.
For this lesson, your code is a home kitchen. Yours — the one you'll cook in for the next ten years.
Video: Uncle Bob's SOLID Principles Made Easy - In Python! — ArjanCodes
All five SOLID principles with practical Python examples and companion code — the "why bother" case from a veteran educator.
What good design actually buys you
Before the five letters, the payoff. Object-oriented design earns its keep in three ways, and every principle below serves at least one of them:
- Maintainability. When the recipe changes, you change it in one place. Not five.
- Testability. You can practice with a toy oven instead of firing up the real kitchen — test the recipe without the heat.
- Changeability. You can add a pasta attachment to the mixer without rebuilding the mixer.
That's the whole sales pitch. A codebase where change is cheap, testing is easy, and nothing is secretly holding up three other things.
Now the five principles. Each one gets one kitchen analogy, then the precise definition underneath.
Video: SOLID Principles Explained in 8 Minutes | Software Design Basics — Knowledge Thrusters
Shows what SOLID buys you — maintainable, scalable code — with real-world examples and common mistakes.
S — Single responsibility: one job per gadget
Somewhere in a catalog there's an all-in-one breakfast station: toaster on the left, griddle in the middle, coffee maker on the right, one power cord. It looks efficient until the toaster element dies — and takes your morning coffee down with it. Three jobs, one box, one shared fate.
Classes work the same way. Martin's phrasing is exact: a class should have one, and
only one, reason to change. Not "do one thing" — change for one reason. A
ReportGenerator that also sends emails and writes to the database has three reasons
to change: the report format, the email policy, the schema. Every change risks the
other two.
// Three reasons to change, one class to blame
class OrderManager {
takeOrder() { /* ... */ } // changes when ordering changes
cookFood() { /* ... */ } // changes when the menu changes
printBill() { /* ... */ } // changes when billing changes
}
The fix is the split you'd expect:
flowchart LR
G[KitchenGod<br/>cooks, serves, bills]:::service --> C[Cook]:::service
G --> S[Server]:::service
G --> B[Cashier]:::service
Cook, Server, Cashier. The menu changes? You touch the Cook. Billing changes? The Cashier. Nothing else flinches. That's maintainability, bought with a sketch you could draw on a napkin.
See it side by side:
Interactive diagram: VsToggle (loads in the app)
And your smoke detector? It has exactly one job — scream when there's smoke — and it has never once tried to also be a clock radio. Be like the smoke detector. (It'll be back.)
Video: Ignacy Sokołowski: Single Responsibility Principle - PyWaw Summit 2015 — PyWaw Summit
Conference talk on how to spot mixed responsibilities and refactor code toward single-purpose, testable classes.
O — Open/closed: the stand mixer
Your stand mixer is a motor with a socket on top. Pasta roller, meat grinder, grain mill, ice cream churn — every attachment snaps into the same socket. You've never once opened the motor housing to add a new one.
That's the open/closed principle, and the phrase comes from Bertrand Meyer (Object-Oriented Software Construction, 1997): software entities should be open for extension, but closed for modification. You add behavior by adding new code, not by editing old code.
The usual violation is the switch statement that grows a new case every sprint:
function charge(order) {
switch (order.paymentType) {
case 'card': /* ... */ break;
case 'cash': /* ... */ break;
case 'crypto': /* ... */ break; // this month's edit
// next month: crack this function open again
}
}
Every new payment method means opening a function that already works — and risking
every payment method that already works. The fix is the mixer socket: depend on a
PaymentMethod interface, and let each new method arrive as a new attachment. New code
gets added; old code stays shut. That's changeability, and it's the reason the mixer
outlives every attachment trend.
Video: Open/Closed Principle (OCP) Explained with Java | SOLID Principles with Real-World Example — asknehru
Uses a payment-service example to show adding new behavior via polymorphism instead of editing existing code.
L — Liskov substitution: the cream test
A recipe calls for cream. You're out, so you reach for oat cream. Fine — if it behaves like cream everywhere the recipe needs cream: it shouldn't curdle under heat, and it should whip. If your substitute curdles in the pan, it wasn't really a substitute. It just looked like one on the shelf.
That's Liskov substitution, from Barbara Liskov's 1987 keynote "Data Abstraction
and Hierarchy": objects of a subtype must be usable anywhere the supertype is expected,
without breaking the program. It's about behavior, not labels. The classic violation:
Square extends Rectangle, but Rectangle lets you set width and height
independently — and a square that silently changes its height when you set its width
just broke every recipe written for rectangles.
The kitchen version: if the recipe says "any pan," your non-stick pan had better survive the broiler the way the cast iron would. A substitute that can't keep the contract isn't a substitute — it's a surprise. LSP is the principle that says surprises are bugs.
Liskov violations in the wild
This isn't a textbook curiosity — two of the most-used classes in Java history violate
it. java.util.Stack extends Vector, so a stack inherits methods like
insertElementAt that let you jam an element into the middle — shattering the
last-in-first-out contract every stack recipe depends on. java.util.Properties
extends Hashtable, inheriting put(Object, Object), which happily accepts
non-string keys that getProperty will then never find. In both cases the subclass
looks substitutable and quietly isn't: the oat cream that curdles in the pan.
Joshua Bloch holds both up in Effective Java as cautionary tales for the same
verdict — inheritance models "is-a," and when the "is-a" is a lie, every caller pays.
The test is always behavioral: can a caller use the subtype without knowing it isn't the supertype? If answering that requires reading the subclass's source code, the contract is already broken.
Video: Liskov Substitution Principle (LSP) Explained in Java | SOLID Principles Made Simple — Byte & Beyond with Uday
Uses Bird, Sparrow, and Ostrich to demonstrate substitutable subclasses and fix inheritance that breaks expectations.
I — Interface segregation: the right menu
Your TV remote has eighty buttons. You use nine. The other seventy-one are a maze you navigate every time you want the volume up — and every one of them is a button some client is forced to depend on.
Interface segregation says: no client should be forced to depend on methods it doesn't use. Hand the vegan the vegan menu, not the steakhouse menu with a lecture. In code: prefer several small, focused interfaces over one fat one.
// The 80-button remote
interface Worker { work(): void; eat(): void; sleep(): void; }
class Robot implements Worker {
work() { /* ... */ }
eat() { /* throw? */ } // robots don't eat. Now what?
sleep() { /* ... */ }
}
The robot is forced to implement eat() — a method it can never honestly provide —
because the interface was designed for humans. Split the interface and the problem
evaporates:
interface Workable { work(): void; }
interface Eatable { eat(): void; }
class Human implements Workable, Eatable { /* ... */ }
class Robot implements Workable { /* ... */ } // no fake eat() required
Small interfaces, honest clients. Nobody implements a method just to throw "not supported" into the void.
Video: Interface Segregation Principle | Design Principles - SOLID | Low Level Design — Your Tech Buddy
Explains why fat interfaces cause trouble and how splitting them into focused contracts improves flexibility.
D — Dependency inversion: plug, don't hardwire
Your lamp plugs into a wall outlet. It is not hardwired to the power plant. When you move apartments, the lamp comes with you — because it depends on the socket, an abstraction, not on any particular grid.
Dependency inversion says the same about code: high-level modules should depend on
abstractions, not on low-level details — and the details should depend on the
abstractions too. The recipe app shouldn't name your hard drive. It should name a
Storage interface, and let the hard drive (or the cloud bucket, or the test fake)
plug into that.
flowchart TB
App[Recipe App]:::service --> I[Storage<br/>interface]:::service
Disk[Local Disk]:::data -. implements .-> I
Cloud[Cloud Bucket]:::cloud -. implements .-> I
Notice the arrows: the low-level details point at the abstraction. That's the "inversion" — in naive code, the app would point straight at the disk, and swapping storage would mean rewriting the app.
Watch a swap happen, step by step:
Interactive diagram: StepThrough (loads in the app)
Video: Dependency Inversion Principle (D) - Software Design Patterns — The Software Mentor
Depend on abstractions, not concretions — a working engineer walks through the DIP with TypeScript code, when it pays off, and when it is overkill.
The junk drawer tour: violations you'll meet in the wild
You now have names for the messes. A quick field guide:
- The God class (violates S): the kitchen junk drawer with a class name —
OrderManagerdoing ordering, cooking, billing, and email. Every change is archaeology. - The switch-statement farm (violates O): a function that grows a new case every sprint. You can smell it from the hallway.
- Inheritance for reuse (violates L):
Penguin extends Bird— then someone callsfly()and the penguin files a complaint. If the subtype can't keep the contract, don't inherit; compose. - The 80-button remote (violates I): one fat interface, half the clients faking methods they can't honestly implement.
- The hardwired lamp (violates D): the app that names its database in twelve places. Moving apartments now requires an electrician.
And the smoke detector? Still on the ceiling. Still one job. Still never broken a build.
Video: 6 Minutes to Finally Understand SOLID Principles — Neural Download
Walks through classic violations like the 2000-line God class plus pitfalls where over-applying SOLID backfires.
Takeaways
- SOLID is about cheap change. Maintainability (one place to edit), testability (swap the real thing for a fake), changeability (add without rewriting) — every principle serves at least one.
- Single responsibility: a class should have one, and only one, reason to change. Split the breakfast station into a toaster, a griddle, and a coffee maker.
- Open/closed: open for extension, closed for modification (Meyer). Add attachments to the mixer; never open the motor housing.
- Liskov substitution: a subtype must be usable anywhere its supertype is expected, without breaking the program. The oat cream must survive the pan.
- Interface segregation: no client should depend on methods it doesn't use. Hand the vegan the vegan menu.
- Dependency inversion: depend on abstractions, not details. Plug the lamp into the outlet; don't hardwire it to the power plant.
Check your understanding
Your ReportGenerator class also sends the report by email and saves a copy to the database. Which principle is it violating?
- The open/closed principle — it can't be extended with new report types
- The Liskov substitution principle — it isn't substitutable for its parent
- The dependency inversion principle — it depends on low-level details
- The single responsibility principle — it has three reasons to change
In the stand-mixer analogy, what does the mixer's attachment socket represent?
- The stable interface that new behavior plugs into without modifying existing code
- A base class that every new attachment is forced to inherit from
- The database where the mixer stores the recipes it knows
- A performance trick for making the motor run faster
Square extends Rectangle, but setting a square's width also changes its height — breaking code written for rectangles. What does this violate?
- Interface segregation — clients are forced to depend on methods they don't use
- Liskov substitution — the subtype can't stand in for the supertype
- The open/closed principle — someone modified the rectangle class
- Dependency inversion — the square depends on a concrete class
A Robot class is forced to implement eat() even though robots don't eat. What's the interface-segregation fix?
- Make eat() throw an error — at least that's honest about it
- Delete the Robot class entirely and only build for humans
- Split the fat interface into smaller ones, so Robot only implements what it uses
- Merge eat() and work() into one doEverything() method
Why should the recipe app depend on a Storage interface instead of directly on LocalDisk?
- Interfaces execute faster than concrete classes ever could
- So the app can swap in a cloud bucket or a test fake without changing its own code
- Because the compiler requires every class to declare an interface
- To make the class diagram symmetrical, which impresses reviewers
Go deeper
Want to keep pulling this thread? These talks and tutorials go further than we did here:
- GORUCO 2009 - SOLID Object-Oriented Design by Sandi Metz — Sandi Metz, GoRuCo 2009. All five principles with practical examples and the TDD payoff.
- SOLID principles, Clean Architecture, Clean Code and more: "Behind Software" with Robert C. Martin — Robert C. Martin, "Behind Software" interview series, YouTube (~90 min). The principles' creator clears up what SRP really means and which principle he'd drop.
- SOLID Principles Interview Questions | Explained with C# Real Example — YouTube tutorial, YouTube (~18 min). Each principle shown as violation-then-fix in real code, not just rules.
- Master Design Patterns & SOLID Principles in C# – Full OOP Course for Beginners — freeCodeCamp.org (~11–12h). Jump to the SOLID chapter for all five principles with real code examples.
Sources & further reading
- Robert C. Martin, Agile Software Development: Principles, Patterns, and Practices (Prentice Hall, 2002) — collects the five principles under the SOLID acronym.
- Robert C. Martin, Clean Architecture (Prentice Hall, 2017) — restates the principles, including the "one, and only one, reason to change" framing of single responsibility.
- Bertrand Meyer, Object-Oriented Software Construction, 2nd ed. (Prentice Hall, 1997) — the open/closed principle: open for extension, closed for modification.
- Barbara Liskov, "Keynote address — data abstraction and hierarchy," OOPSLA '87 (published in ACM SIGPLAN Notices, vol. 23, no. 5, 1988) — the substitution property behind the Liskov substitution principle.
- The acronym "SOLID" was coined around 2004 by Michael Feathers (as noted on Wikipedia's SOLID article and Martin's own Clean Architecture, p. 58) — Martin originated the five principles, Feathers named them.