Skip to main content

A complete guide to SOLID Design Principles.

In software development, code often becomes complex and difficult to maintain as projects grow. Without proper design principles, developers encounter problems such as tightly coupled modules, duplicated code, and difficulty in introducing new features without breaking existing functionality.

The SOLID principles of object-oriented programming help overcome these challenges by promoting clean architecture and modular design. They provide clear guidelines that make applications more flexible, easier to understand, and simpler to extend or refactor. By applying SOLID, developers can build robust, maintainable, and scalable software systems.

1. S – Single Responsibility Principle (SRP)

A class should have only one reason to change.

In other words, each class should focus on a single responsibility. This reduces the risk of side effects and makes the code easier to maintain.

In real-world applications, classes often tend to grow over time, accumulating multiple responsibilities. For example, a Car class might start by handling driving, but then developers often add methods for fueling, repairing, and logging trips. While this seems convenient at first, it quickly becomes difficult to maintain. Every time a fueling mechanism changes or a repair logic is updated, you have to modify the same Car class. This increases the chance of introducing bugs into unrelated functionalities.

The Single Responsibility Principle, as defined by Robert C. Martin (Uncle Bob), encourages splitting responsibilities into separate classes or modules, each with one reason to change. This design leads to modular, easier-to-test, and more maintainable code. It also promotes reusability, since independent classes can often be used in different contexts without modification.

Applying SRP is not just about splitting classes arbitrarily. It requires analyzing the domain and understanding which responsibilities are truly independent. For instance, driving a car is fundamentally different from refueling or repairing it. Therefore, keeping these concerns in separate classes ensures that changes in one area do not impact others.

Below is a practical example showing a class that violates SRP, followed by a refactored version adhering to the principle.


// Class violating SRP

public class Car {
    public void drive() {
        System.out.println("Driving the car");
    }

    public void refuel(int liters) {
        System.out.println("Adding " + liters + " liters");
    }

    public void performRepair(String issue) {
        System.out.println("Repairing: " + issue);
    }
}

In this design, Car has multiple responsibilities: driving, fueling, and repairing. If any of these responsibilities change, the Car class must be modified, violating SRP.


// Refactored version following SRP

public class Car {
    public void drive() {
        System.out.println("Driving the car");
    }
}

public class FuelService {
    public void refuel(Car car, int liters) {
        System.out.println("Adding " + liters + " liters to car");
    }
}

public class RepairService {
    public void repair(Car car, String issue) {
        System.out.println("Repairing: " + issue);
    }
}

With this design, each class has a single responsibility:

  • Car → responsible only for driving.
  • FuelService → responsible only for fueling.
  • RepairService → responsible only for repairs.

This separation ensures modularity, easier maintenance, and reduces the risk of side effects when changes are made to one part of the system. It also makes the code easier to understand and test, improving overall software quality.

2. O – Open/Closed Principle (OCP)

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

Put simply, a class or module should allow new functionality to be added without requiring changes to its existing code. This ensures stability while enabling the software to evolve over time.

Consider a scenario in an e-commerce system where we need to calculate shipping costs for different types of orders. Initially, we might have a single ShippingCalculator class handling standard and express shipping.

Problem 1 – Initial design violating OCP


// Single class handling multiple shipping types
public class ShippingCalculator {
    public double calculate(String type, double weight) {
        if (type.equals("STANDARD")) 
        {
            return weight * 1.0;
        } 
        else if (type.equals("EXPRESS"))
        {
            return weight * 2.0;
        }
        // Adding a new shipping type will require modifying this class
        return 0;
    }
}

In this design, every time a new shipping method is introduced (e.g., overnight shipping), the ShippingCalculator class must be modified. This is risky and violates the Open/Closed Principle.

Solution – Refactoring using OCP

Instead of modifying the existing class, we can define a ShippingStrategy interface and create separate classes for each shipping type. This way, adding new shipping methods only requires adding new classes.


// Interface defining the contract
public interface ShippingStrategy {
    double calculate(double weight);
}

// Standard shipping implementation
public class StandardShipping implements ShippingStrategy {
    public double calculate(double weight) {
        return weight * 1.0;
    }
}

// Express shipping implementation
public class ExpressShipping implements ShippingStrategy {
    public double calculate(double weight)
    {
        return weight * 2.0;
    }
}

// Shipping processor that uses the strategy
public class ShippingProcessor {
    private final ShippingStrategy strategy;

    public ShippingProcessor(ShippingStrategy strategy) 
    {
        this.strategy = strategy;
    }

    public double process(double weight) 
    {
        return strategy.calculate(weight);
    }
}

Now, each shipping type is implemented in its own class:

  • StandardShipping → calculates standard shipping
  • ExpressShipping → calculates express shipping
  • ShippingProcessor → remains unchanged when new shipping types are added

With this approach:

  • The system is open for extension – you can add new shipping strategies easily.
  • The system is closed for modification – existing code does not need to be touched.

Breaking the problem into smaller responsibilities and using a strategy interface makes the code modular, maintainable, and less prone to errors when extending functionality in the future.

Popular posts from this blog

SDE SHEET - Code Quest 135+

Crafted for MAANG aspirants, this DSA sheet features 135+ challenges—from warm-ups to hard-level problems. Each question will have a detailed video lecture on our YouTube channel. (Video links will be added shortly) Making it your ultimate last-minute prep companion. Arrays & Hashing PRACTICE SOLUTION Two Sum Valid Anagram Contains Duplicate Maximum Points You Can Obtain Move Zeros Check if Array Is Sorted and Rotated Find the Duplicate Number Product of Array Except Self First Missing Positive Best Time to Buy and Sell Stock Longest Consecutive Sequence Merge Intervals Matrix PRACTICE SOLUTION Set Matrix Zeroes Spiral Matrix Rotate Image Val...

Resources – Spring Boot, Microservices, LLD, HLD

Welcome to my curated Resources Hub – your one-stop destination for learning and building real-world backend applications. Here, I share all the tools, courses, projects, and references I personally use to teach and work on scalable systems. Whether you're exploring Spring Boot for the first time, diving into Low-Level or High-Level Design concepts, or looking for practical projects to build your skills, you'll find carefully selected material here. This page is continuously updated as I discover new resources, lectures, and courses – so check back often to see the latest additions and maximize your learning journey.