DEV Community

Deepika Pusala
Deepika Pusala

Posted on

SOLID Principles Deep Dive (S, O, L): Explained from Absolute Zero ๐Ÿงฑ

๐Ÿงฑ SOLID Principles Deep Dive (S, O, L): Let's Learn from ZERO!

SOLID shows up in Python, FastAPI, Java, Spring Boot, React architecture and almost every real-world codebase. Interviewers and mentors rarely stop at "define SOLID". They ask why we use it, what problem it solves, and how you apply it in a project. ๐ŸŽฏ

In this post we'll cover the first three principles, S, O and L, step by step, with simple examples and practical code. ๐Ÿš€


1. ๐Ÿค” First, What Are SOLID Principles?

Imagine you're building an Instagram-like social media app with registration and login, posts, image uploads, likes, comments, notifications and follows. At first you write everything in one huge class:

class SocialMedia:
    def register_user(self):
        pass

    def login_user(self):
        pass

    def create_post(self):
        pass

    def upload_image(self):
        pass

    def like_post(self):
        pass

    def comment_on_post(self):
        pass

    def send_notification(self):
        pass
Enter fullscreen mode Exit fullscreen mode

It works, right? But when the app grows to 100 features:

  • ๐Ÿ˜ต Changing login might accidentally affect registration
  • ๐Ÿ˜ต Changing image upload might affect post creation
  • ๐Ÿงช Testing individual features becomes difficult
  • โ™ป๏ธ Reusing functionality becomes difficult
  • ๐ŸŒ Adding new features gets complicated

This is tightly coupled, poorly organized code. We need a better way to design software. That's where SOLID comes in!

๐Ÿ”ค What does SOLID stand for?

Letter Principle One-line idea
S Single Responsibility One class should have one responsibility
O Open/Closed Open for extension, closed for modification
L Liskov Substitution Child classes should safely replace parent classes
I Interface Segregation Don't force classes to implement methods they don't need
D Dependency Inversion Depend on abstractions, not concrete implementations

Today we'll go deep on S, O and L.


2. 1๏ธโƒฃ S: Single Responsibility Principle (SRP)

2.1 ๐Ÿค” What does it mean?

๐Ÿ“Œ A class should have only one reason to change.

  • Single = one
  • Responsibility = one specific job
  • Principle = a rule to follow

In simple English: a class should focus on one particular job instead of handling several unrelated ones.

๐Ÿฝ๏ธ Real-life example: a restaurant chef prepares food. The chef should not also manage customer payments, clean the whole restaurant, maintain the database or handle salaries. Those jobs belong to other people or departments. When one responsibility changes, it shouldn't unnecessarily affect everything else. The same logic applies to classes.

2.2 โŒ SRP with bad code

A user can register, be saved to a database, and receive a welcome email. A beginner might write:

class User:
    def register(self, username, email):
        # Validate user
        print("User registered")

        # Save user to database
        print("Saving user to database")

        # Send welcome email
        print("Sending welcome email")
Enter fullscreen mode Exit fullscreen mode

This class has three different responsibilities:

Functionality Responsibility
Validate user Business validation
Save user Database operations
Send email Email communication

Now imagine:

  • ๐Ÿ“ง The email provider changes from Gmail SMTP to SendGrid โ†’ modify User
  • ๐Ÿ—„๏ธ The database changes from PostgreSQL to MySQL โ†’ modify User
  • โœ… Validation rules change โ†’ modify User

The class has multiple reasons to change. That's an SRP violation. ๐Ÿšจ

2.3 โœ… Correct implementation using SRP

Separate responsibilities into different classes:

UserService     โ†’ handles user registration business logic
UserRepository  โ†’ database operations
EmailService    โ†’ email operations
Enter fullscreen mode Exit fullscreen mode

Class 1: UserRepository (only database operations)

class UserRepository:
    def save_user(self, username, email):
        print("Saving user to database")
Enter fullscreen mode Exit fullscreen mode

Class 2: EmailService (only sending emails)

class EmailService:
    def send_welcome_email(self, email):
        print("Sending welcome email")
Enter fullscreen mode Exit fullscreen mode

Class 3: UserService (coordinates the registration process)

class UserService:
    def __init__(self, repository, email_service):
        self.repository = repository
        self.email_service = email_service

    def register_user(self, username, email):
        print("Validating user")

        self.repository.save_user(username, email)
        self.email_service.send_welcome_email(email)

        print("Registration completed")
Enter fullscreen mode Exit fullscreen mode

Run it:

repository = UserRepository()
email_service = EmailService()

user_service = UserService(
    repository,
    email_service,
)

user_service.register_user(
    "Deepika",
    "deepika@gmail.com",
)
Enter fullscreen mode Exit fullscreen mode
Validating user
Saving user to database
Sending welcome email
Registration completed
Enter fullscreen mode Exit fullscreen mode

Each class now has a clear responsibility.

๐Ÿ“ UserService still coordinates several steps, but its responsibility is the registration use case. Having multiple method calls doesn't automatically violate SRP.

2.4 โœ… Why is SRP important?

Benefit Explanation
๐Ÿ› ๏ธ Maintainability Easier to modify individual functionality
โ™ป๏ธ Reusability Reuse EmailService in other modules
๐Ÿงช Testability Test each class independently
๐Ÿ“– Readability Code becomes easier to understand
๐Ÿ›ก๏ธ Reduced side effects Changes are less likely to break unrelated features

2.5 โšก SRP in a FastAPI backend

backend/
โ””โ”€โ”€ app/
    โ”œโ”€โ”€ api/
    โ”‚   โ””โ”€โ”€ routes/
    โ”‚       โ”œโ”€โ”€ auth.py
    โ”‚       โ””โ”€โ”€ posts.py
    โ”‚
    โ”œโ”€โ”€ services/
    โ”‚   โ”œโ”€โ”€ auth_service.py
    โ”‚   โ””โ”€โ”€ post_service.py
    โ”‚
    โ”œโ”€โ”€ repositories/
    โ”‚   โ”œโ”€โ”€ user_repository.py
    โ”‚   โ””โ”€โ”€ post_repository.py
    โ”‚
    โ””โ”€โ”€ models/
        โ”œโ”€โ”€ user.py
        โ””โ”€โ”€ post.py
Enter fullscreen mode Exit fullscreen mode
Layer Responsibility
๐Ÿ›ฃ๏ธ API routes Receive HTTP requests and return responses
๐Ÿง  Services Handle business logic
๐Ÿ“š Repositories Communicate with the database
๐Ÿ—„๏ธ Models Represent database entities

For example: auth.py handles login and registration endpoints, auth_service.py handles authentication business logic, user_repository.py handles user database operations, and user.py represents the User database model. This separation supports SRP.

๐ŸŽค Interview question: What happens if we put database queries, business logic and HTTP request handling inside one route function?

Answer: The route becomes responsible for too many things. It becomes harder to test, maintain, reuse and modify independently.


3. 2๏ธโƒฃ O: Open/Closed Principle (OCP)

3.1 ๐Ÿค” What does it mean?

๐Ÿ“Œ Software entities should be open for extension but closed for modification.

It sounds confusing at first, so let's take each phrase:

  • Open for extension โ†’ we can add new functionality
  • Closed for modification โ†’ we avoid repeatedly changing existing, working code to support every new requirement

โš ๏ธ It does NOT mean existing code can never be changed. It means designing code so that adding a feature usually doesn't require modifying existing, stable logic.

๐Ÿ’ณ Real-life example: a payment app first supports credit and debit cards. Later the company adds UPI, PayPal and net banking. A poorly designed system forces you to change the same payment-processing function every time. A better design lets you add new payment methods without rewriting the existing payment flow.

3.2 โŒ OCP with bad code

class PaymentService:
    def pay(self, payment_type, amount):
        if payment_type == "card":
            print("Processing card payment")
        elif payment_type == "upi":
            print("Processing UPI payment")
        elif payment_type == "paypal":
            print("Processing PayPal payment")
Enter fullscreen mode Exit fullscreen mode

It works! But tomorrow we add Google Pay, so we modify pay(). Next month another method, and we modify pay() again. The method keeps growing with new conditions, which makes it harder to maintain and increases the chance of breaking existing functionality.

3.3 โœ… Correct implementation using OCP

We use abstraction and polymorphism. Don't worry, we'll explain both.

First, a common interface using Python's ABC. An interface is essentially a contract that says which methods an implementation must provide.

from abc import ABC, abstractmethod


class PaymentMethod(ABC):
    @abstractmethod
    def pay(self, amount):
        pass
Enter fullscreen mode Exit fullscreen mode
  • ABC lets us define an abstract base class
  • @abstractmethod declares a method that child classes must implement
  • PaymentMethod defines the common contract

Now the different payment methods:

class CardPayment(PaymentMethod):
    def pay(self, amount):
        print(f"Paid {amount} using Card")


class UPIPayment(PaymentMethod):
    def pay(self, amount):
        print(f"Paid {amount} using UPI")


class PayPalPayment(PaymentMethod):
    def pay(self, amount):
        print(f"Paid {amount} using PayPal")
Enter fullscreen mode Exit fullscreen mode

The payment service:

class PaymentService:
    def process_payment(self, payment_method, amount):
        payment_method.pay(amount)
Enter fullscreen mode Exit fullscreen mode

Run it:

service = PaymentService()

card = CardPayment()
upi = UPIPayment()
paypal = PayPalPayment()

service.process_payment(card, 1000)
service.process_payment(upi, 500)
service.process_payment(paypal, 2000)
Enter fullscreen mode Exit fullscreen mode
Paid 1000 using Card
Paid 500 using UPI
Paid 2000 using PayPal
Enter fullscreen mode Exit fullscreen mode

Now the business introduces Google Pay. We simply add another class:

class GooglePay(PaymentMethod):
    def pay(self, amount):
        print(f"Paid {amount} using Google Pay")
Enter fullscreen mode Exit fullscreen mode
google_pay = GooglePay()
service.process_payment(google_pay, 1500)
Enter fullscreen mode Exit fullscreen mode
Paid 1500 using Google Pay
Enter fullscreen mode Exit fullscreen mode

Look carefully: we did not modify the existing PaymentService class. We extended the system by adding a new implementation. OCP achieved! ๐ŸŽ‰

3.4 ๐ŸŽญ What is polymorphism here?

๐Ÿ“Œ Polymorphism means one common interface, many different implementations.

payment_method.pay(amount)
Enter fullscreen mode Exit fullscreen mode

The same method call behaves differently depending on the object passed:

Object What happens?
CardPayment Card payment
UPIPayment UPI payment
PayPalPayment PayPal payment
GooglePay Google Pay payment

Same method name, different behavior. That's polymorphism.

3.5 โœ… Why is OCP important?

  • โž• Adding new features becomes easier
  • ๐Ÿงฑ Existing tested code stays stable
  • ๐Ÿ” Reduces repeated modifications
  • ๐Ÿ“ˆ Improves scalability
  • ๐Ÿ›ก๏ธ Reduces the risk of introducing bugs into existing functionality

3.6 ๐Ÿ“ฑ OCP in a social media app

Your app supports email notifications at first. Later you add push, SMS and in-app notifications. Instead of one huge function with many if/elif branches, create a common notification interface. Each notification type implements its own sending behavior. When a new type arrives, you add a new implementation and don't rewrite the main notification-processing logic.

๐ŸŽค Interview question: Why is OCP useful in a growing application?

Answer: Requirements change frequently. OCP lets developers add new functionality through extension instead of repeatedly modifying existing, stable code, reducing the risk of breaking existing features.


4. 3๏ธโƒฃ L: Liskov Substitution Principle (LSP)

Probably the trickiest of the three at first. But once you see the example, it's easy. ๐Ÿ˜„

4.1 ๐Ÿค” What does it mean?

๐Ÿ“Œ Objects of a parent class should be replaceable with objects of its child classes without breaking the correctness of the program.

  • Parent class = base class
  • Child class = derived class
  • Substitution = replacing one thing with another

In simple English: if a child class inherits from a parent class, the child should behave in a way that is compatible with what the parent promises. Wherever you use a child object instead of a parent object, the application should keep working correctly.

๐Ÿงฌ First, understand inheritance

Inheritance is when one class acquires properties and methods from another:

class Animal:
    def eat(self):
        print("Animal is eating")


class Dog(Animal):
    def bark(self):
        print("Dog is barking")
Enter fullscreen mode Exit fullscreen mode

Animal is the parent, Dog is the child, and Dog inherits eat():

dog = Dog()
dog.eat()
dog.bark()
Enter fullscreen mode Exit fullscreen mode
Animal is eating
Dog is barking
Enter fullscreen mode Exit fullscreen mode

But LSP asks something more important: can we safely use the child class wherever the parent class is expected?

4.2 ๐Ÿฆ Real-life example: birds

class Bird:
    def fly(self):
        print("Bird is flying")


class Sparrow(Bird):
    def fly(self):
        print("Sparrow is flying")


class Eagle(Bird):
    def fly(self):
        print("Eagle is flying")
Enter fullscreen mode Exit fullscreen mode

Both can fly, so everything is fine. Now:

def make_bird_fly(bird):
    bird.fly()


make_bird_fly(Sparrow())
make_bird_fly(Eagle())
Enter fullscreen mode Exit fullscreen mode
Sparrow is flying
Eagle is flying
Enter fullscreen mode Exit fullscreen mode

The child objects substitute for the parent type without problems. That's LSP. โœ…

4.3 โŒ Understanding an LSP violation

Now we introduce a Penguin. Can a penguin fly? No! But our parent class says every Bird has a fly() method.

class Penguin(Bird):
    def fly(self):
        raise Exception("Penguins cannot fly")


penguin = Penguin()
make_bird_fly(penguin)
Enter fullscreen mode Exit fullscreen mode
Exception: Penguins cannot fly
Enter fullscreen mode Exit fullscreen mode

make_bird_fly() expects a bird that can fly. Penguin inherits from Bird but breaks that expectation. The parent promises flying behavior, and the child can't deliver it. This is a classic Liskov Substitution Principle violation. ๐Ÿšจ

4.4 โœ… How do we fix it?

Redesign the hierarchy. Instead of saying every Bird can fly, separate general bird behavior from flying behavior:

class Bird:
    def eat(self):
        print("Bird is eating")


class FlyingBird(Bird):
    def fly(self):
        print("Bird is flying")


class Sparrow(FlyingBird):
    def fly(self):
        print("Sparrow is flying")


class Eagle(FlyingBird):
    def fly(self):
        print("Eagle is flying")


class Penguin(Bird):
    def swim(self):
        print("Penguin is swimming")
Enter fullscreen mode Exit fullscreen mode
Bird           โ†’ common bird behavior: eat()
 โ”œโ”€โ”€ FlyingBird โ†’ fly()
 โ”‚     โ”œโ”€โ”€ Sparrow
 โ”‚     โ””โ”€โ”€ Eagle
 โ””โ”€โ”€ Penguin    โ†’ swim()   (does not promise flying behavior)
Enter fullscreen mode Exit fullscreen mode

Now:

def make_bird_fly(bird: FlyingBird):
    bird.fly()


make_bird_fly(Sparrow())
make_bird_fly(Eagle())
Enter fullscreen mode Exit fullscreen mode

We don't pass Penguin. The design now reflects the actual capabilities of the objects.

๐Ÿ’ก LSP isn't simply about whether inheritance exists. It's about whether the child class honors the behavioral contract of the parent class.

4.5 ๐Ÿ“ Another important example: Rectangle and Square

A famous interview example. Mathematically a square is a special kind of rectangle, so a developer might try this:

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def set_width(self, width):
        self.width = width

    def set_height(self, height):
        self.height = height

    def area(self):
        return self.width * self.height


class Square(Rectangle):
    def set_width(self, width):
        self.width = width
        self.height = width

    def set_height(self, height):
        self.width = height
        self.height = height
Enter fullscreen mode Exit fullscreen mode

Test it:

def calculate_area(rectangle):
    rectangle.set_width(5)
    rectangle.set_height(10)
    return rectangle.area()
Enter fullscreen mode Exit fullscreen mode

With a normal rectangle:

rectangle = Rectangle(2, 3)
print(calculate_area(rectangle))   # 50 โœ…
Enter fullscreen mode Exit fullscreen mode

With a square:

square = Square(2, 2)
print(calculate_area(square))      # 100 โŒ
Enter fullscreen mode Exit fullscreen mode

Why? Setting the height to 10 also changes the square's width to 10. The function expected a rectangle whose width and height can change independently, and Square breaks that expectation. That's an LSP violation.

๐Ÿง  The lesson: a mathematically valid inheritance relationship doesn't always make a good software inheritance relationship.

4.6 ๐Ÿ” How to spot LSP violations

Warning signs:

  1. A child method throws an exception for behavior the parent promises to support
  2. A child method does nothing when the parent promises meaningful behavior
  3. A child changes the expected output or behavior in an incompatible way
  4. A child requires stricter inputs than the parent
  5. A child returns weaker or incompatible results than the parent contract promises

There are deeper formal rules behind this:

  • Preconditions should not be strengthened
  • Postconditions should not be weakened
  • Parent invariants should be preserved
Term Simple meaning
Preconditions What must be true before a method runs
Postconditions What the method guarantees after execution
Invariants Rules that must stay true throughout the object's valid lifetime

For example, if a parent method accepts any positive number, a child shouldn't suddenly accept only numbers greater than 100. That would strengthen the precondition and violate LSP.

4.7 โœ… Why is LSP important?

  • ๐Ÿ”’ Makes inheritance reliable
  • ๐Ÿšซ Prevents unexpected runtime errors
  • โ™ป๏ธ Improves code reusability
  • ๐ŸŽญ Makes polymorphism safer
  • ๐Ÿ—๏ธ Helps you design correct class hierarchies
  • ๐Ÿงช Makes unit testing easier, because child objects can be tested against parent expectations

5. โš–๏ธ Comparing S, O and L

Principle Main question Problem it prevents
S: SRP Does this class have one clear responsibility? Classes doing too many unrelated jobs
O: OCP Can I add features without repeatedly modifying stable code? Constant modification of existing logic
L: LSP Can a child object replace its parent safely? Incorrect inheritance and unexpected behavior

One example to remember each:

  • ๐Ÿ…ข SRP: a User class shouldn't handle everything. Separate database operations, email sending and registration.
  • ๐Ÿ…ž OCP: adding Google Pay shouldn't require rewriting PaymentService. Add a new implementation of the common contract.
  • ๐Ÿ…› LSP: a Penguin shouldn't be forced to implement flying. Design inheritance around actual behavioral capabilities.

6. ๐Ÿค How Are These Principles Related?

They're separate, but they complement each other. Imagine an e-commerce backend with PaymentService, EmailService, OrderService, PaymentMethod, CardPayment and UPIPayment:

  • SRP: each class has a well-defined responsibility
  • OCP: you can introduce a new payment method without changing the existing payment-processing logic
  • LSP: every payment implementation honors the common PaymentMethod contract, so the payment service can safely use any of them

One good design can follow all three principles at once. โœจ


7. โš ๏ธ Common Mistakes with SOLID

Following SOLID doesn't mean creating hundreds of unnecessary classes.

Mistake Correct understanding
"SRP means one method per class" No. It means one cohesive responsibility per class
"OCP means never modify existing code" No. It means designing for extension without unnecessary modification
"LSP means every child must have exactly the same code as its parent" No. Children can implement things differently but must preserve expected behavior
"More classes always mean better SOLID design" No. Avoid unnecessary complexity
"Inheritance automatically means LSP is satisfied" No. Behavioral compatibility is what matters

8. ๐ŸŽค Interview Questions and Answers

Q1. What is SOLID?
A collection of five object-oriented design principles that help developers write maintainable, flexible, reusable and scalable software.

Q2. What's the difference between SRP and OCP?
SRP ensures a class has one clear responsibility. OCP allows new functionality to be added without repeatedly modifying existing, stable code.

Q3. What does "one reason to change" mean in SRP?
A class should be responsible for one cohesive area of functionality. If that responsibility changes, the class may need modification.

Q4. How do we implement OCP in Python?
Using abstraction, abstract base classes, inheritance and polymorphism to create extensible designs.

Q5. What is polymorphism?
It lets different objects respond to the same method call in their own ways.

Q6. What happens when LSP is violated?
A child object may cause unexpected errors or incorrect behavior when used in place of its parent object.

Q7. Is a Square a Rectangle? Why can it violate LSP?
Mathematically yes. But if a Rectangle lets width and height change independently, a Square that forces them to stay equal violates the expected behavior of that implementation.

Q8. Are SOLID principles only for Java?
No. They apply to object-oriented design in Python, Java, C++, C# and other languages, and their ideas also help when designing larger application architectures.


9. ๐Ÿงช Test Your Understanding!

Try these before peeking at the answers! โค๏ธ

  1. A class handles user registration, database operations and email sending. Which principle is violated?

    • A) OCP
    • B) SRP
    • C) LSP
  2. You add Google Pay by creating a new payment class without modifying PaymentService. Which principle are you following?

    • A) SRP
    • B) LSP
    • C) OCP
  3. A Penguin inherits from Bird, but the parent promises every bird can fly. Which principle is violated?

    • A) LSP
    • B) OCP
    • C) SRP
  4. Different payment classes implement the same pay() method differently. What OOP concept is this?

    • A) Encapsulation
    • B) Polymorphism
    • C) Normalization
  5. A child class requires stricter input conditions than its parent method. Which principle may be violated?

    • A) SRP
    • B) OCP
    • C) LSP

๐Ÿ‘€ Click to reveal answers
  1. B: SRP. The class has several unrelated reasons to change
  2. C: OCP. You extended the system without modifying existing code
  3. A: LSP. The child can't honor the parent's behavioral contract
  4. B: Polymorphism. One interface, many implementations
  5. C: LSP. Strengthening preconditions breaks substitutability


10. ๐Ÿ“ Final Revision Notes

  • ๐Ÿ…ข Single Responsibility Principle: one class = one clear responsibility = one main reason to change
  • ๐Ÿ…ž Open/Closed Principle: open for extension, closed for unnecessary modification
  • ๐Ÿ…› Liskov Substitution Principle: child objects must be safely usable wherever their parent objects are expected

๐Ÿ’ก Don't just memorize the definitions. Whenever you write a FastAPI service, repository, model or authentication module, ask yourself:

  1. Is this class doing too many jobs?
  2. Can I add a new feature without rewriting existing logic?
  3. If I inherit from another class, will my child class preserve its expected behavior?

That's how you start thinking like a software engineer, not just someone who writes code. ๐Ÿง 


If this post helped you, drop a โค๏ธ or ๐Ÿฆ„ and share it with a friend learning software design. Happy coding! ๐Ÿ‘ฉโ€๐Ÿ’ปโœจ

Top comments (0)