Technology

SOLID Principles OOP | Complete Guide | InventiveHQ

Master the SOLID principles of Object-Oriented Design to write cleaner, more maintainable, and scalable code with practical examples.

By Inventive HQ Team

SOLID Principles OOP | Complete Guide | InventiveHQ

SOLID is a set of five object-oriented design principles — Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion — that together reduce the cost of changing software by keeping classes small, contracts narrow, and dependencies pointed at abstractions instead of concrete implementations. The principles were articulated by Robert C. Martin (Uncle Bob) in the late 1990s and early 2000s, with the memorable acronym ordering suggested by Michael Feathers. Applied well, they turn "every change breaks three other things" into "new features are new files."

That is the summary an AI Overview gives you. Here is what it can't show you: which principle actually solves the mess in front of you, what each one looks like as before-and-after code rather than a slogan, and how to tell a principle that's earning its keep from an abstraction you added out of habit. Below is a one-screen decision table, an animated map of how the five principles connect, working Python examples for all five (not just SRP), and a pre-commit checklist you can actually run.

The five principles at a glance

Most guides list SOLID as five definitions and stop. The definitions are the easy part — the hard part is knowing which principle a given smell is telling you to reach for. Use the symptom column to jump to the right letter.

PrincipleOne-line ruleSymptom it fixesReach for it when...
S — Single ResponsibilityA class should have one reason to changeA class edited for two unrelated reasons (DB schema and report layout)Bug fixes in one feature keep breaking an unrelated feature in the same class
O — Open/ClosedOpen for extension, closed for modificationA growing if/elif chain edited for every new typeAdding a case means touching stable, already-tested code
L — Liskov SubstitutionSubtypes must be swappable for their base typeA subclass that throws or no-ops on inherited methodsCallers need isinstance checks to avoid a subclass blowing up
I — Interface SegregationNo client forced to depend on unused methodsImplementers raising NotImplementedError on fat interfacesA "simple" implementer must stub out half the interface
D — Dependency InversionDepend on abstractions, not concretionsHigh-level logic hard-wired to a specific DB/API/vendorYou can't unit-test business logic without a real database

Which do I learn first? SRP and DIP give the biggest payoff for the least ceremony — get those two into muscle memory before worrying about the subtler L and I. The other three fall out naturally once you're separating responsibilities and depending on abstractions.

How the five principles connect

SOLID isn't five unrelated rules — they reinforce each other. Splitting responsibilities (S) creates the seams where you introduce abstractions (D), and those abstractions are what let you extend without modifying (O). The animation below traces that flow.

How the five SOLID principles reinforce each other Single Responsibility creates seams, Dependency Inversion points them at abstractions, Open/Closed extends through them, while Liskov and Interface Segregation keep the abstractions honest — all reducing the cost of change. The SOLID feedback loop Each principle makes the next one cheaper to apply S Single Responsibility Split into small classes → creates clean seams D Dependency Inversion Point the seams at an abstraction, not a class O Open / Closed Add a new implementation, never edit tested code L Liskov Substitution Subtypes stay swappable — keeps abstractions honest I Interface Segregation Narrow contracts — nobody stubs unused methods Result: new features become new files

Understanding SOLID Principles

SOLID is an acronym representing five key principles that address common design challenges in object-oriented programming:

  • Single Responsibility Principle (SRP): A class should have only one reason to change
  • Open/Closed Principle (OCP): Software entities should be open for extension but closed for modification
  • Liskov Substitution Principle (LSP): Subtypes should be replaceable for their base types without altering behavior
  • Interface Segregation Principle (ISP): Clients should not be forced to depend on interfaces they do not use
  • Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules; both should depend on abstractions

The examples below use Python, but every pattern maps directly to Java, C#, TypeScript, or any language with interfaces and classes.

Advertisement

S — Single Responsibility Principle

A class should have only one reason to change. When a class mixes concerns — say, holding user data and saving it to a database and formatting a report — a change to the report layout forces you to reopen (and risk breaking) database code. Split the responsibilities.

# Violation: three unrelated reasons to change one class
class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

    def save_to_database(self):   # changes when the DB schema changes
        print(f"Saving {self.name} to database")

    def generate_report(self):    # changes when the report layout changes
        print(f"Generating report for {self.name}")

# Better: one reason to change per class
class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

class UserRepository:
    def save(self, user: User):
        print(f"Saving {user.name} to database")

class UserReportGenerator:
    def generate_report(self, user: User):
        print(f"Generating report for {user.name}")

O — Open/Closed Principle

Software should be open for extension but closed for modification. The tell-tale violation is an if/elif chain that grows every time you add a type — each edit risks the branches that already work. Replace the chain with polymorphism so new behavior arrives as a new class, not an edit.

# Violation: every new shape edits area()
def area(shape):
    if shape.kind == "circle":
        return 3.14159 * shape.radius ** 2
    elif shape.kind == "rectangle":
        return shape.width * shape.height
    # add a triangle? edit this function and re-test everything

# Better: add a triangle by writing a new class, touching nothing above
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self) -> float: ...

class Circle(Shape):
    def __init__(self, radius): self.radius = radius
    def area(self): return 3.14159 * self.radius ** 2

class Rectangle(Shape):
    def __init__(self, w, h): self.w, self.h = w, h
    def area(self): return self.w * self.h

L — Liskov Substitution Principle

Any code that works with a base type must keep working when handed a subtype — without knowing which subtype it got. A subclass that breaks its parent's guarantees (throwing where the base never would, or violating an invariant) forces callers into defensive isinstance checks.

# Violation: a Square breaks Rectangle's contract
class Rectangle:
    def __init__(self, w, h): self._w, self._h = w, h
    def set_width(self, w):  self._w = w
    def set_height(self, h): self._h = h
    def area(self): return self._w * self._h

class Square(Rectangle):        # a Square *is not* substitutable
    def set_width(self, w):  self._w = self._h = w   # surprises callers
    def set_height(self, h): self._w = self._h = h

# Any function expecting to set width and height independently now breaks.
# Fix: don't inherit. Model Square and Rectangle as separate Shapes (see OCP).

I — Interface Segregation Principle

Clients should not be forced to depend on methods they never use. A "fat" interface makes simple implementers stub out irrelevant methods with NotImplementedError. Split it into role-focused interfaces so each client depends only on what it actually calls.

# Violation: a fat interface forces a robot to "eat"
class Worker(ABC):
    @abstractmethod
    def work(self): ...
    @abstractmethod
    def eat(self): ...

class Robot(Worker):
    def work(self): print("working")
    def eat(self):  raise NotImplementedError  # robots don't eat

# Better: split roles so nobody stubs unused methods
class Workable(ABC):
    @abstractmethod
    def work(self): ...

class Eatable(ABC):
    @abstractmethod
    def eat(self): ...

class Human(Workable, Eatable):
    def work(self): print("working")
    def eat(self):  print("eating")

class Robot(Workable):          # depends only on what it uses
    def work(self): print("working")

D — Dependency Inversion Principle

High-level modules (your business logic) should not depend on low-level modules (a specific database or vendor SDK); both should depend on an abstraction. Invert the arrow: the policy defines the interface it needs, and the detail implements it. This is what makes business logic unit-testable without a live database.

# Violation: NotificationService is welded to email
class EmailClient:
    def send(self, msg): print(f"email: {msg}")

class NotificationService:
    def __init__(self):
        self.client = EmailClient()      # can't swap, can't fake in tests
    def notify(self, msg): self.client.send(msg)

# Better: depend on an abstraction, inject the concrete detail
class MessageSender(ABC):
    @abstractmethod
    def send(self, msg): ...

class EmailClient(MessageSender):
    def send(self, msg): print(f"email: {msg}")

class SmsClient(MessageSender):
    def send(self, msg): print(f"sms: {msg}")

class NotificationService:
    def __init__(self, sender: MessageSender):   # inject the dependency
        self.sender = sender
    def notify(self, msg): self.sender.send(msg)

# Production wiring and a test fake are now a one-line swap.
NotificationService(EmailClient()).notify("deploy done")

A pre-commit SOLID checklist

Run this against a class before you open the pull request. Any "yes" is a prompt to refactor — not a mandate, but a question worth answering.

  1. SRP: Does this class change for more than one reason? Could I name two different stakeholders who would each request edits here?
  2. OCP: To add the next obvious variant, would I edit this file — or add a new one? A growing if/elif on a type/kind field is the red flag.
  3. LSP: Does any subclass throw, no-op, or pass on a method its base type promises? Do callers use isinstance to dodge a subclass?
  4. ISP: Does any implementer raise NotImplementedError or leave methods empty? Split the interface along those lines.
  5. DIP: Can I unit-test this class's logic without a real database, network, or vendor SDK? If not, the dependency is concrete where it should be abstract.
  6. The over-engineering check: Does each abstraction I added have (or credibly foresee) a second implementation? A one-implementation interface is usually premature.

Benefits of Applying SOLID Principles

Implementing SOLID principles leads to significant improvements in code quality and maintainability:

  • Improved maintainability: Easier to modify and debug individual components
  • Enhanced reusability: Modular classes can be reused across different projects
  • Better testability: Smaller, focused components (and injected dependencies) make unit testing straightforward
  • Reduced technical debt: Clean, organized code prevents future refactoring crunches
  • Improved scalability: Extensible design supports growing requirements without rewrites

Key takeaway: SOLID principles are not theoretical decoration — they solve real change-management problems and keep systems flexible as requirements evolve.

Common Mistakes and Best Practices

While SOLID provides excellent guidance, it can be misapplied. The failures to avoid:

  • Overcomplicating code: Breaking everything into tiny classes and interfaces before any real variation exists
  • Modifying instead of extending: Violating OCP by editing tested classes for every new case
  • Improper inheritance: Creating subclasses that break base-class expectations (the LSP trap)
  • Fat interfaces: Forcing implementers to stub methods they never call
  • Tight coupling: High-level modules depending directly on low-level implementations

Remember: SOLID should enhance code quality, not manufacture complexity. Apply each principle in response to actual pain — duplication, churn, untestable logic — rather than preemptively on code that isn't hurting yet.

Elevate Your IT Efficiency with Expert Solutions

Transform Your Technology, Propel Your Business

Master advanced software architecture and design patterns with professional guidance. At InventiveHQ, we combine programming expertise with innovative cybersecurity practices to enhance your development skills, streamline your IT operations, and leverage cloud technologies for optimal efficiency and growth.

Discover Our Services

Frequently Asked Questions

What does SOLID stand for?

SOLID is an acronym for five object-oriented design principles: Single Responsibility (a class should have one reason to change), Open/Closed (open for extension, closed for modification), Liskov Substitution (subtypes must be usable in place of their base type), Interface Segregation (don't force clients to depend on methods they don't use), and Dependency Inversion (depend on abstractions, not concrete implementations). The letters were popularized by Robert C. Martin around 2000; Michael Feathers coined the "SOLID" ordering.

Who invented the SOLID principles?

Robert C. Martin (Uncle Bob) articulated the five principles across papers in the late 1990s and early 2000s. The memorable "SOLID" acronym itself was suggested by Michael Feathers, who reordered Martin's principles so the first letters spelled a word. Liskov Substitution is older still, coming from Barbara Liskov's 1987 keynote on data abstraction.

Do I have to use all five SOLID principles at once?

No. SOLID is a toolkit, not a checklist you must satisfy on every class. In practice you reach for a principle when a specific pain appears: split a class (SRP) when it changes for two unrelated reasons, add an abstraction (OCP/DIP) when a growing if/elif chain keeps forcing edits to stable code, or split an interface (ISP) when implementers keep raising NotImplementedError. Applying all five preemptively to trivial code usually adds indirection without benefit.

What is the difference between SRP and ISP?

Single Responsibility applies to classes and modules — a class should have one reason to change. Interface Segregation applies to interfaces (or abstract base classes) — clients should not be forced to implement or depend on methods they never call. SRP is about who a class serves; ISP is about keeping the contracts between classes narrow so a change to one capability doesn't ripple into unrelated consumers.

Are SOLID principles only for object-oriented languages?

The vocabulary is object-oriented, but the underlying ideas — small units, stable abstractions, and loose coupling — translate to functional and procedural code. In functional programming, Dependency Inversion becomes passing functions as arguments, and Open/Closed becomes composing higher- order functions rather than editing a switch statement. The principles describe change management, not a specific language feature.

How does the Open/Closed Principle work in practice?

You make a module extensible through new code rather than edits to existing code. The classic pattern is defining an abstraction (an interface or abstract base class) and adding new behavior by writing a new implementation of it, instead of adding another branch to an if/elif chain. When done well, shipping a new feature means adding a file, not touching — and risking regressions in — code that already works and is already tested.

Can you over-apply SOLID and make code worse?

Yes. The most common failure is splitting code into many tiny classes and interfaces before there is any real variation to justify them, which buries simple logic under layers of indirection. SOLID should reduce the cost of change; if an abstraction only ever has one implementation and no second one is on the roadmap, it is usually premature. Apply the principles in response to actual duplication or churn, not speculatively.

What is the Liskov Substitution Principle in simple terms?

If code works with a base type, it must keep working when you hand it any subtype without knowing which one it got. A subclass must not strengthen preconditions, weaken postconditions, or throw exceptions the base type never would. The canonical violation is a Square that inherits from Rectangle: setting width and height independently, which Rectangle allows, breaks Square's own invariant and surprises any code expecting Rectangle behavior.

soliddesign patternsobject-oriented