DEV Community

Cover image for SOLID Design Principle
Gokul G.K.
Gokul G.K.

Posted on

SOLID Design Principle

What is SOLID?

SOLID stands for: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion. Robert C. Martin popularized these principles, and they are now widely used as the foundation for maintainable object-oriented design.

SOLID is a way to manage dependencies between classes, allowing you to add features without breaking existing Code. Empirical work shows that applying SOLID tends to improve modularity, reduce coupling, and make it easier for developers to understand and extend Code.
These are some example in Java for the SOLID principle.

Single Responsibility Principle (SRP)

A class should have one, and only one, reason to change.
Single Responsibility Principle
The chef's knife is a good example of SRP, as a tool with a single, focused responsibility: cutting. It doesn't try to be a spoon or a whisk.

package SOLID;
//A class should have only one reason to change, meaning it should have only one responsibility.
public class SingleResponsibilityPrinciple {
    //Example violating SRP
    class Employee {
        public void calculatePay() {
            //Code to calculate pay
        }
        public void saveEmployeeData() {
            //Code to save employee data to the database
        }
    }
    // Refactored classes adhering to SRP
    class PayCalculator {
        public void calculatePay() {
            //Code to calculate pay
        }
    }
    class EmployeeDataSaver {
        public void saveEmployeeData() {
            //Code to save employee data to the database
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In the above code, the first version (Employee class) violates SRP because it has multiple responsibilities as it both calculates an employee's pay and saves employee data to the database.
After refactoring, these responsibilities are split into two separate classes (PayCalculator and EmployeeDataSaver), so each class has only one reason to change and follows the Single Responsibility Principle.

Open/Closed Principle (OCP)

Classes should be open for extension, but closed for modification.
Open/Closed Principle
A Lego brick is another good example; it's closed for modification, but can be modified by adding other bricks.

package SOLID;
/**
 * Software entities (classes, modules, functions) should be open for extension but closed for modification.
 * */
public class OpenClosePrinciple {
    //Example violating OCP
    abstract class Rectangle {
        public double area(double length, double width) {
            return length * width;
        }
    }
    // Extending Rectangle  and  modifying are
    class Square extends Rectangle {
        public double area(double side) {
            return side * side;
        }
    }
    // OCP Strategy  Use proper abstraction (most common & clean)
    interface Shape {
        double area();           // closed for modification
    }
    // Now we can add any new shape without touching the existing Code
    class Rectangleocp implements Shape {
        private double length;
        private double width;

        public Rectangleocp(double length, double width) {
            this.length = length;
            this.width = width;
        }
        @Override
        public double area() {
            return length * width;
        }
    }
    class Squareocp implements Shape {
        private double side;

        public Squareocp(double side) {
            this.side = side;
        }
        @Override
        public double area() {
            return side * side;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In the code above, the bad Example (inheritance from concrete Rectangle) forces the modification of existing Code or awkward design when adding new shapes.
The reasonable approach is to use a stable Shape interface that allows you to add any new shape by writing new classes without ever touching existing ones.

Liskov Substitution Principle (LSP)

Subtypes must be substitutable for their base types without breaking the program's expectations
Liskov Substitution Principle
Another real-life example is a universal power strip acts as a "superclass," and any plug type (US, UK, EU) is a "subclass" that can be used with it without any issues, as they all fulfill the contract of providing a power connection.

package SOLID;

/**
 * Subtypes must be substitutable for their base types without affecting the correctness of the program.
 * **/
public class LiskovSubstitutionPrinciple {
    // Example violating LSP (classic Rectangle-Square problem)
    static class Rectangle {
        protected int width;
        protected int height;
        public void setWidth(int width) {
            this.width = width;
        }
        public void setHeight(int height) {
            this.height = height;
        }
        public int area() {
            return width * height;
        }
    }
    static class Square extends Rectangle {
        @Override
        public void setWidth(int width) {
            this.width = width;
            this.height = width;   // Forces height = width
        }
        @Override
        public void setHeight(int height) {
            this.height = height;
            this.width = height;   // Forces width = height
        }
    }
    // Fixed version - separate classes implementing a common interface (recommended)
    interface Shape {
        int area();
    }
    static class RectangleFixed implements Shape {
        private int width;
        private int height;

        public RectangleFixed(int width, int height) {
            this.width = width;
            this.height = height;
        }
        public void setWidth(int width)   { this.width = width; }
        public void setHeight(int height) { this.height = height; }

        @Override
        public int area() {
            return width * height;
        }
    }
    static class SquareFixed implements Shape {
        private int side;

        public SquareFixed(int side) {
            this.side = side;
        }
        public void setSide(int side) {
            this.side = side;
        }
        @Override
        public int area() {
            return side * side;
        }
    }
    // One-liner usage example showing the violation vs fixed behavior
    public static void main(String[] args) {
        // Violating version - unexpected result when treating Square as Rectangle
        Rectangle r = new Square();
        r.setWidth(5); r.setHeight(10);                // Area should be 50, but becomes 100!
        // Fixed version - safe and predictable
        Shape rect = new RectangleFixed(5, 10);        // Area = 50
        Shape sq   = new SquareFixed(5);               // Area = 25
    }
}
Enter fullscreen mode Exit fullscreen mode

The first part shows the classic LSP violation where Square extends Rectangle and overrides setters, breaking substitutability because a Square used as a Rectangle no longer behaves as expected (area becomes incorrect).
The fixed version removes inheritance between Rectangle and Square, instead having both implement a common Shape interface, ensuring any Shape can be substituted safely.

Interface Segregation Principle (ISP)

Clients should not be forced to depend on methods they do not use.
Interface Segregation Principle

package SOLID;

/**
 * Clients should not be forced to depend on methods they do not use. Interfaces should be specific to the client's needs.
 * */
public class InterfaceSegregation {
    //Example violating ISP
    interface Worker {
        void work();
        void eat();
    }
    class Robot implements Worker {
        @Override
        public void work() {
            //Code for the robot to work
        }
        @Override
        public void eat() {
            // Robots don't eat, an unnecessary method for them
        }
    }
    // Refactored into segregated interfaces
    interface Workable {
        void work();
    }
    interface Feedable {
        void eat();
    }
    class RobotRefactored implements Workable {
        @Override
        public void work() {
            //Code for the robot to work
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

In the bad Example, Robot is polluted with an irrelevant eat() method just because it implements a fat Worker interface.
By splitting the interface into smaller, purpose-specific ones (Workable and Feedable), we eliminate unnecessary dependencies and make the design cleaner and more maintainable.

Dependency Inversion Principle (DIP)

Depend on abstractions, not on concrete implementations.
Dependency Inversion Principle

package SOLID;
/**
 * High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.
 * **/
public class DependencyInversion {
    //Example violating DIP
    class EmailService {
        public void sendEmail(String message, String recipient) {
            //Code to send email
        }
    }
    class UserManager {
        private final EmailService emailService;

        public UserManager() {
            this.emailService = new EmailService();
        }
        public void sendWelcomeEmail(String user) {
            emailService.sendEmail("Welcome!", user);
        }
    }
    // Refactored with abstraction
    interface MessageService {
        void sendMessage(String message, String recipient);

        void sendEmail(String s, String user);
    }
    class EmailServiceRefactored implements MessageService {
        @Override
        public void sendMessage(String message, String recipient) {
            //Code to send email
        }
        @Override
        public void sendEmail(String s, String user) {
        }
    }
    class UserManagerRefactored {
        private MessageService messageService;

        public void UserManager(MessageService messageService) {
            this.messageService = messageService;
        }
        UserManagerRefactored(MessageService messageService) {
            this.messageService = messageService;
        }
        public void sendWelcomeMessage(String user) {
            messageService.sendMessage("Welcome!", user);
        }
    }

}
Enter fullscreen mode Exit fullscreen mode

In the bad Example, UserManager is tightly coupled to EmailService — making it hard to change or test.
In the refactored version, we invert the dependency; both high-level (UserManager) and low-level (EmailService, SmsService, etc.) depend on the MessageService abstraction, achieving loose coupling and flexibility.

How to tie SOLID into "system design."

  • Scaling teams: SOLID supports modular ownership, where different teams can evolve different modules with fewer cross‑impacts. ​
  • Evolving requirements: OCP and DIP support changing external systems or adding new features with localized changes. ​
  • Reliability and testing: Smaller, single‑responsibility classes are easier to test and mock, which helps build confidence when changing complex systems.

In brief: Why follow SOLID?
"It makes code easier to maintain, test, and extend as requirements change."

All images generated using nano banana.

Top comments (1)

Collapse
 
gokul_gk profile image
Gokul G.K.

Git Repo