A solid explanation to SOLID Principle

Published: (December 31, 2025 at 07:03 AM EST)
7 min read
Source: Dev.to

Source: Dev.to

Introduction

A piece of software rarely fails because “the algorithm was wrong.”
More often it degrades slowly: features become hard to add, bugs re‑appear, and even a tiny change feels risky.

The SOLID principles give us five practical ways to write code that stays understandable and adaptable for a significantly longer period of time.

SOLID is not one of the three states of matter.
It is an acronym for five design principles that help us write highly maintainable code.

The principles are:

  1. Single Responsibility Principle (SRP)
  2. Open‑Closed Principle (OCP)
  3. Liskov Substitution Principle (LSP)
  4. Interface Segregation Principle (ISP)
  5. Dependency Inversion Principle (DIP)

Together, they encourage loose coupling, clear responsibilities, and stable abstractions—essential for long‑lived code.

1. Single Responsibility Principle (SRP)

A class should have one and only one reason to change.

Bad example

class UserService {
    public void validate(User user) { /* … */ }
    public void saveToDatabase(User user) { /* … */ }
    public void sendEmail(User user) { /* … */ }
    public void log(User user) { /* … */ }
}

Problems

  • Four reasons to change (validate, saveToDatabase, sendEmail, log).
  • Hard to reuse – callers don’t want to trigger email and DB writes when they only need validation.
  • Testing nightmare – you must mock logger, email client, and DB client.

Better design

class UserValidator {
    public void validate(User user) { /* … */ }
}

class UserRepository {
    public void save(User user) { /* … */ }
}

class EmailNotifier {
    public void send(User user) { /* … */ }
}

class Logger {
    public void log(User user) { /* … */ }
}

Each class now has one reason to change and can be used independently (e.g., validation without emailing).

Mantra: Ek class – ek kaam (One class – one job).

2. Open‑Closed Principle (OCP)

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

Violating OCP

class AreaCalculator {
    public double calculateArea(Object shape) {
        if (shape instanceof Rectangle) {
            Rectangle r = (Rectangle) shape;
            return r.width * r.height;
        } else if (shape instanceof Circle) {
            Circle c = (Circle) shape;
            return Math.PI * c.radius * c.radius;
        }
        // Adding a Square would require modifying this method → OCP violation
    }
}

OCP‑compliant design

abstract class Shape {
    abstract double area();
}

class Rectangle extends Shape {
    double width, height;
    @Override double area() { return width * height; }
}

class Circle extends Shape {
    double radius;
    @Override double area() { return Math.PI * radius * radius; }
}

// New shapes (e.g., Square) just extend Shape – no change to AreaCalculator
class AreaCalculator {
    public double calculateArea(Shape shape) {
        return shape.area();
    }
}

Now we can add a Square (or any new shape) without touching AreaCalculator.

Mantra: Extend karo, edit nahi (Extend, don’t edit).

3. Liskov Substitution Principle (LSP)

Objects of a superclass shall be replaceable with objects of a subclass without altering the desirable properties of the program.

Problematic example

interface Bird {
    void fly();
    void walk();
}

class Sparrow implements Bird {
    public void fly() { /* flying logic */ }
    public void walk() { /* walking logic */ }
}

class Ostrich implements Bird {
    public void fly() { throw new UnsupportedOperationException(); }
    public void walk() { /* walking logic */ }
}

Ostrich breaks the contract of fly() – code that expects any Bird to fly will fail.

Better abstraction

interface Walkable {
    void walk();
}

interface Flyable {
    void fly();
}

abstract class Bird implements Walkable { /* common walk logic */ }

class Sparrow extends Bird implements Flyable {
    public void fly() { /* flying logic */ }
}

class Ostrich extends Bird {
    // No fly() method – respects LSP
}

Now makeWalk(Bird b) works for both Sparrow and Ostrich without unexpected behavior.

Memory aid (Hindi): Agar bachche ka behavior parents ke jaise hai, to wo party mein ja sakta hai (If a subclass behaves like its parent, it can be used interchangeably).

4. Interface Segregation Principle (ISP)

Clients should not be forced to depend on interfaces they do not use.

Violating ISP

interface IWorker {
    void work();
    void eat();   // Robots don’t eat!
}

A Robot class would have to implement a meaningless eat() method, creating dummy code and tight coupling.

ISP‑compliant design

interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

class Human implements Workable, Eatable {
    public void work() { /* … */ }
    public void eat()  { /* … */ }
}

class Robot implements Workable {
    public void work() { /* … */ }
}

Now each class implements only the interfaces it actually needs.

Mantra: Divide and Rule – split large interfaces into focused ones.

5. Dependency Inversion Principle (DIP)

High‑level modules should not depend on low‑level modules; both should depend on abstractions.

Tight‑coupled example

class MasalaTeaMaker {
    public String makeTea() { return "Masala Tea ready!"; }
}

class TeaShop {
    private MasalaTeaMaker maker = new MasalaTeaMaker(); // Hard‑coded dependency!

    public String orderTea(String type) {
        if ("masala".equals(type)) {
            return maker.makeTea();
        }
        return "Only Masala available"; // Rigid!
    }
}

TeaShop is directly tied to MasalaTeaMaker. Adding a new tea type would require changing TeaShop.

DIP‑compliant design

interface TeaMaker {
    String makeTea();
}

class MasalaTeaMaker implements TeaMaker {
    public String makeTea() { return "Masala Tea ready!"; }
}

class GreenTeaMaker implements TeaMaker {
    public String makeTea() { return "Green Tea ready!"; }
}

class TeaShop {
    private final Map<String, TeaMaker> makers = new HashMap<>();

    public TeaShop(Map<String, TeaMaker> makers) {
        this.makers.putAll(makers);
    }

    public String orderTea(String type) {
        TeaMaker maker = makers.get(type);
        return (maker != null) ? maker.makeTea() : "Tea not available";
    }
}
  • TeaShop depends on the abstraction TeaMaker.
  • New tea makers can be added without modifying TeaShop; just register them in the map.

Summary

PrincipleCore IdeaTypical Mantra
SRPOne reason to change per classEk class – ek kaam
OCPExtend behavior without modifying existing codeExtend karo, edit nahi
LSPSubtypes must be substitutable for their base typesBachche ka behavior parents ke jaise
ISPSplit fat interfaces into client‑specific onesDivide and Rule
DIPDepend on abstractions, not concretionsHigh‑level ↔ low‑level via interfaces

Applying these five principles helps us build loosely coupled, highly cohesive, and easily maintainable software that can evolve gracefully over time.

Updated TeaShop Example Using Abstraction

Below is a cleaned‑up version of the original snippet.
The code now follows the Dependency Inversion Principle (the “D” in SOLID) by depending on an abstraction (TeaMaker) instead of concrete classes.

1️⃣ Domain Model (Java)

// 1. Abstraction -------------------------------------------------
public interface TeaMaker {
    String makeTea();
}

// 2. Concrete implementations --------------------------------------
public class MasalaTeaMaker implements TeaMaker {
    @Override
    public String makeTea() {
        return "Masala Tea ready!";
    }
}

public class GingerTeaMaker implements TeaMaker {
    @Override
    public String makeTea() {
        return "Ginger Tea ready!";
    }
}

// 3. High‑level module (depends on the abstraction) -------------
public class TeaShop {

    // Constructor injection (setter injection is also possible)
    private final TeaMaker teaMaker;

    public TeaShop(TeaMaker teaMaker) {
        this.teaMaker = teaMaker;
    }

    public String orderTea(String type) {
        // The shop no longer knows about concrete makers.
        // It simply asks the injected TeaMaker to prepare the drink.
        return teaMaker.makeTea();
    }
}

2️⃣ How It Works

ComponentResponsibilityDependency
TeaMakerDefines the contract for any tea‑making class.
MasalaTeaMaker / GingerTeaMakerConcrete implementations that fulfil the TeaMaker contract.Implements TeaMaker
TeaShopHigh‑level class that orders tea.Depends only on TeaMaker (abstraction).

The TeaShop no longer creates or knows about MasalaTeaMaker or GingerTeaMaker. Instead, a specific TeaMaker is supplied (injected) at runtime.

3️⃣ Dependency Injection Options

Injection TypeExample
Constructor injection (shown above)new TeaShop(new MasalaTeaMaker())
Setter injectionjava\npublic void setTeaMaker(TeaMaker teaMaker) { this.teaMaker = teaMaker; }\n
Field injection (framework‑specific)@Inject private TeaMaker teaMaker;

Choose the style that best fits your project’s architecture.

4️⃣ Quick Memory Aid

“Abstraction pe depend karo, class pe nahi.”

Think of it as “depend on the interface, not the concrete class.” This mantra keeps you aligned with SOLID’s Dependency Inversion Principle.

5️⃣ Closing Note

With the above refactor we have satisfied the SOLID principles, especially D (Dependency Inversion).
If you have any questions, feel free to drop a comment below.

Thanks for reading – stay tuned!

Back to Blog

Related posts

Read more »