SOLID Principles Explained as Simply as Possible (+ Examples)

Aug 12, 2025 5 min read
SOLID Principles Explained as Simply as Possible (+ Examples)

If you’ve been coding for a while, you’ve probably run into that moment where you look at a piece of your own code and think:

“Who wrote this? … oh no, it was me.”

It’s normal. Projects grow, requirements change, and before you know it, your clean little script has turned into an unappetizing pile of spaghetti.

The SOLID principles are basically a set of common-sense guidelines to help you avoid that situation.

SOLID is an acronym for:

  1. Single Responsibility Principle
  2. Open/Closed Principle
  3. Liskov Substitution Principle
  4. Interface Segregation Principle
  5. Dependency Inversion Principle

They sound a bit academic, but the ideas are very simple. Let’s walk through each one, with examples in Python that actually make sense.

Why should I know SOLID?

The SOLID principles are often learned alongside design patterns. Decades ago, programmers couldn't easily google "top programming tips" or use LLMs to refactor their code.

This had two considerations:

  1. Best practices weren't fully mapped out yet.
  2. Information was harder to pass around, and was mostly found in textbooks.

So, early developers had to develop a common language for good, clean code. SOLID was created to help write clean, object-oriented code.

Although the principles have complicated names, SOLID is quite easy to understand, and will supercharge your code readability and maintainability.

1. Single Responsibility Principle (SRP)

The rule:

A class should have one reason to change.

The Single Responsibility Principle is basically saying: don’t make a class do too much. Every class should have just one main reason to exist, and only one type of change should affect it. If a class is juggling multiple unrelated tasks, changes to one part of it can accidentally break another. This makes debugging harder and creates hidden dependencies in your code. By keeping responsibilities separate, you get code that’s easier to test, maintain, and extend without side effects.

Think of it like this: if you had to explain to a new developer what a class does, you should be able to do it in one sentence. If you start saying “Well, it also handles…”, you’ve probably broken SRP.

Example of ignoring SRP

[@portabletext/react] Unknown block type "myCodeField", specify a component for it in the `components.types` prop

Here, Report:

  1. Knows how to format a report.
  2. Knows how to save it.

If saving changes (e.g., now you need to save to a database or send over an API), you have to touch this class, even if the report format didn’t change. That’s coupling two unrelated concerns.

Following SRP

[@portabletext/react] Unknown block type "myCodeField", specify a component for it in the `components.types` prop

Now:

  • Report deals with content.
  • ReportSaver deals with persistence.

This makes each one easier to test, change, and reuse.

2. Open/Closed Principle (OCP)

The rule:

Software entities should be open for extension, but closed for modification.

The Open/Closed Principle is about designing your code so that you can add new functionality without touching the existing, stable code. If every time you want to support a new feature you have to crack open an old class and change it, you’re risking introducing bugs. Instead, you write your classes so they can be extended (usually via inheritance or composition) without rewriting what already works. This makes your codebase safer to evolve, especially in larger projects where many people are working on different parts. It’s like adding new Lego bricks instead of reshaping the old ones.

You shouldn’t have to change existing code just to add new behavior. Instead, you should be able to extend it.

Breaking OCP

[@portabletext/react] Unknown block type "myCodeField", specify a component for it in the `components.types` prop

If we add a “student” discount, we have to edit this class, risking breaking something.

OCP-friendly approach

[@portabletext/react] Unknown block type "myCodeField", specify a component for it in the `components.types` prop

Now, adding a new discount type doesn’t require editing existing code — just make a new class.

3. Liskov Substitution Principle (LSP)

The rule:

Subclasses should be replaceable for their base classes without breaking the program.

The Liskov Substitution Principle sounds fancy but is simple in practice: if you have a function that works with a base class, it should work with any subclass without weird surprises. A subclass shouldn’t remove behavior that the base class promised or suddenly change its meaning. If a subclass pretends to be compatible but actually changes expectations (like throwing errors where the base class wouldn’t), it’s a red flag. Following LSP helps keep your class hierarchies predictable and avoids subtle bugs where a subclass "kind of" fits but breaks things in edge cases.

In short: if you write code that works with a base class, it should still work if you give it any subclass.

Violating LSP

Imagine we’re building a payment processing system.

[@portabletext/react] Unknown block type "myCodeField", specify a component for it in the `components.types` prop

Now we add a subclass for PayPal payments:

[@portabletext/react] Unknown block type "myCodeField", specify a component for it in the `components.types` prop

So far, so good.
Then someone creates a subclass for a “Trial” payment option:

[@portabletext/react] Unknown block type "myCodeField", specify a component for it in the `components.types` prop

Uh-oh. This breaks LSP.

If another part of the system says:

[@portabletext/react] Unknown block type "myCodeField", specify a component for it in the `components.types` prop

…it crashes.
The method exists, but its behavior completely changes expectations. Instead of processing a payment, it blows up.

LSP-friendly design

We can fix this by not making TrialPaymentProcessor a payment processor at all — because it’s not capable of doing that.

[@portabletext/react] Unknown block type "myCodeField", specify a component for it in the `components.types` prop

We keep responsibilities clear:

  • Payment processors can always process a payment.
  • Trial accounts are just a different type of user, not a payment processor.

4. Interface Segregation Principle (ISP)

The rule:

Don’t force a class to implement methods it doesn’t use.

The Interface Segregation Principle is about avoiding “fat” interfaces that make classes implement methods they don’t actually need. If you’ve ever seen a class with a bunch of raise NotImplementedError stubs, you’ve probably found an ISP violation. This happens when a single interface or base class tries to cover too many unrelated abilities. Breaking them into smaller, more focused interfaces lets each class pick only the capabilities it really uses. This leads to cleaner designs and fewer useless methods hanging around.

Breaking ISP

[@portabletext/react] Unknown block type "myCodeField", specify a component for it in the `components.types` prop

OldPrinter is forced to have useless methods.

ISP-friendly

[@portabletext/react] Unknown block type "myCodeField", specify a component for it in the `components.types` prop

Now classes only implement what they actually do.

5. Dependency Inversion Principle (DIP)

The rule:

Depend on abstractions, not on concrete implementations.

The Dependency Inversion Principle says that high-level code (big-picture logic) shouldn’t depend directly on low-level code (specific details). Instead, both should depend on abstractions like interfaces or abstract classes. This way, you can swap out the low-level details without rewriting the high-level logic. For example, your application shouldn’t care whether it’s talking to MySQL or PostgreSQL — it should just talk to something that behaves like a database. DIP makes code more modular, testable, and flexible for future changes.

Instead of high-level modules calling specific low-level classes directly, both should depend on a common abstraction.

Without DIP

[@portabletext/react] Unknown block type "myCodeField", specify a component for it in the `components.types` prop

If we switch to PostgreSQL, we must change App.

With DIP

[@portabletext/react] Unknown block type "myCodeField", specify a component for it in the `components.types` prop

Now, switching databases is as simple as passing a different class.

Wrapping It Up

SOLID isn’t a magic formula that guarantees perfect code. You can still write awful code while “following” these rules. But they’re a great mental checklist:

  • SRP: Does this class have only one reason to change?
  • OCP: Can I add new behavior without editing existing code?
  • LSP: Will my subclasses work anywhere the base class works?
  • ISP: Am I forcing classes to implement stuff they don’t need?
  • DIP: Am I depending on abstractions instead of concrete details?

Even applying one or two of these principles can make a big difference in how maintainable your projects feel.

DISCLAIMER: We sometimes discuss medical and mental health topics we're passionate about, but remember: Our website's content is for informational purposes only and is never a substitute for medical advice, diagnosis, or treatment. For any health concerns, please consult a qualified healthcare professional.


Share this post