In object-oriented programming, classes are the fundamental building blocks of any application. When we design these classes poorly, the entire codebase becomes difficult to maintain, extend, and debug over time. SOLID principles give us a structured way to avoid these problems from the start.
The term SOLID was coined by Michael Feathers, building on the work of Robert C. Martin (widely known as Uncle Bob), who introduced these concepts in his 2000 paper Design Principles and Design Patterns. Over the past two decades, these five principles have become the cornerstone of clean, maintainable object-oriented design.
SOLID stands for:
- S – Single Responsibility Principle (SRP)
- O – Open-Closed Principle (OCP)
- L – Liskov Substitution Principle (LSP)
- I – Interface Segregation Principle (ISP)
- D – Dependency Inversion Principle (DIP)
When we follow these principles carefully, we write code that is easier to understand, extend, and refactor. We also reduce technical debt and minimize the number of bugs that creep in as requirements change.
SOLID principles are not strict rules we must always follow to the letter. They are guiding principles that help us make better design decisions, especially as our application grows in size and complexity.

1. Single Responsibility Principle (SRP)
“One class should have one and only one responsibility”
This means every class we write should be responsible for exactly one piece of functionality within the application. When a class takes on multiple responsibilities, any change to one responsibility risks breaking the other. This makes our code fragile and tightly coupled.
Imagine a class that handles user authentication, sends email notifications, and also writes logs to a file. If the logging mechanism changes, we are forced to modify a class that is also responsible for authentication logic. This violates SRP and creates unnecessary risk.
1.1. SRP Violation Example
In the following example, the Employee class above handles domain logic, email communication, and database persistence all in one place. Each of these is a separate responsibility that should live in its own class.
// BAD: This class has too many responsibilities
public class Employee {
public String getDesignation(int employeeId) {
// Fetch designation from DB
return "Software Engineer";
}
public void updateSalary(int employeeId, double salary) {
// Update salary in DB
}
public void sendPayslipEmail(String email) {
// Send email - NOT the Employee class's responsibility
}
public void saveToDatabase() {
// Persistence logic mixed into a domain class
}
}
1.2. SRP Correct Implementation
// GOOD: Each class has a single responsibility
public class Employee {
private int id;
private String name;
private String designation;
private double salary;
// Getters and setters only
}
public class EmployeeRepository {
public void save(Employee employee) {
// Handles DB persistence
}
public Employee findById(int id) {
return new Employee();
}
}
public class NotificationService {
public void sendPayslipEmail(String email, Employee employee) {
// Handles email communication
}
}
Now each class has exactly one reason to change. If the email provider changes, we only touch NotificationService. If the database schema changes, only EmployeeRepository is affected.
We can find plenty of classes in all popular Java libraries which follow the single responsibility principle. For example, in Log4j2, we have different classes with logging methods, different classes are logging levels and so on.
2. Open-Closed Principle (OCP)
The open/closed principle is the second principle we should consider while designing our application. It states:
“Software components should be open for extension, but closed for modification”
It means that the application classes should be designed in such a way that whenever fellow developers want to change the flow of control in specific conditions in the application, all they need to do is extend the class and override some functions, and that’s it.
Modifying existing, tested code to accommodate new features introduces regression risk. OCP guides us to design our classes so that new functionality can be added by writing new code rather than changing existing code.
If we take a look into any good framework like struts or spring, we will see that we cannot change their core logic and request processing. Still, we modify the desired application flow by extending some classes and plugin them in configuration files.
For example, spring framework has class DispatcherServlet. This class acts as a front controller for Spring-based web applications. To use this class, we are not required to modify this class. All we need is to pass initialization parameters, and we can extend its functionality the way we want.
Please note that apart from passing initialization parameters during application startup, we can also override methods to modify the behavior of the target class by extending the classes. For example, struts Action classes are extended to override the request processing logic.
2.1. OCP Violation Example
// BAD: Every new shape requires modifying this class
public class AreaCalculator {
public double calculateArea(Object shape) {
if (shape instanceof Circle) {
Circle c = (Circle) shape;
return Math.PI * c.getRadius() * c.getRadius();
} else if (shape instanceof Rectangle) {
Rectangle r = (Rectangle) shape;
return r.getWidth() * r.getHeight();
}
// Adding a new shape forces us to come back and edit this method
return 0;
}
}
In the above example, every time we add a new shape, we must open this class and modify it. This creates a maintenance burden and increases the chance of introducing bugs in existing, working logic.
2.2. OCP Correct Implementation
// GOOD: Use abstraction to allow extension without modification
public interface Shape {
double calculateArea();
}
public class Circle implements Shape {
private double radius;
public Circle(double radius) { this.radius = radius; }
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
public class Rectangle implements Shape {
private double width, height;
public Rectangle(double w, double h) { this.width = w; this.height = h; }
@Override
public double calculateArea() {
return width * height;
}
}
public class Triangle implements Shape {
private double base, height;
public Triangle(double base, double height) {
this.base = base; this.height = height;
}
@Override
public double calculateArea() {
return 0.5 * base * height;
}
}
public class AreaCalculator {
public double calculateArea(Shape shape) {
return shape.calculateArea();
}
}
Now we can add as many shapes as needed without ever touching the AreaCalculator class. This is the power of designing your classes around abstractions.
3. Liskov Substitution Principle (LSP)
LSP is a variation of the previously discussed open-closed principle. It says:
“Objects of a subclass should be replaceable with objects of their superclass without breaking the correctness of the program.”
This means if class B is a subtype of class A, we should be able to use B wherever A is expected.
LSP guides that the classes, fellow developers created by extending our class, should be able to fit in the application without fail. This is important when we resort to polymorphic behavior through inheritance.
LSP is essentially about designing inheritance hierarchies correctly. When a subclass overrides methods of a parent class in ways that break the expected behavior, we violate LSP and introduce subtle, hard-to-debug errors.
3.1. LSP Violation Example
// BAD: Square violates LSP when extending Rectangle
public class Rectangle {
protected double width;
protected double height;
public void setWidth(double width) { this.width = width; }
public void setHeight(double height) { this.height = height; }
public double getArea() { return width * height; }
}
public class Square extends Rectangle {
@Override
public void setWidth(double width) {
this.width = width;
this.height = width; // Forces both sides to be equal
}
@Override
public void setHeight(double height) {
this.width = height; // Unexpected behavior for a Rectangle consumer
this.height = height;
}
}
// This method breaks when called with a Square
public void testRectangle(Rectangle r) {
r.setWidth(5);
r.setHeight(10);
// Expects 50, but gets 100 if r is a Square
assert r.getArea() == 50;
}
In above example, although a square is mathematically a rectangle, inheriting from Rectangle in code creates unexpected behavior. A consumer of Rectangle has no reason to expect that setting the width would also change the height.
3.2. LSP Correct Implementation
// GOOD: Model the hierarchy around a common abstraction
public interface Shape {
double getArea();
}
public class Rectangle implements Shape {
private double width, height;
public Rectangle(double w, double h) { this.width = w; this.height = h; }
public double getArea() { return width * height; }
}
public class Square implements Shape {
private double side;
public Square(double side) { this.side = side; }
public double getArea() { return side * side; }
}
Both Rectangle and Square now implement a shared abstraction without one extending the other. Any method that expects a Shape will work correctly with either type.
4. Interface Segregation Principle (ISP)
This principle is my favorite one. ISP is applicable to interfaces as a single responsibility principle holds to classes. ISP says:
“Clients should not be forced to implement unnecessary methods which they will not use”
Take an example. Developer Alex created an interface Reportable and added two methods generateExcel() and generatedPdf(). Now client ‘A’ wants to use this interface but he intends to use reports only in PDF format and not in excel. Will he be able to use the functionality easily?
NO. He will have to implement both methods, out of which one is an extra burden put on him by the designer of the software. Either he will implement another method or leave it blank. This is not a good design. Large, bloated interfaces force implementing classes to define methods that are meaningless in their context. This leads to empty implementations, thrown exceptions, and confusion for anyone reading the code.
So what is the solution? The solution is to create two interfaces by breaking the existing one. They should be like PdfReportable and ExcelReportable. This will give the flexibility to users to use only the required functionality only.
4.1. ISP Violation Example
// BAD: One large interface forced on all implementing classes
public interface Worker {
void work();
void eat();
void sleep();
}
// A robot does not eat or sleep!
public class Robot implements Worker {
public void work() { System.out.println("Robot working"); }
public void eat() { /* Robots do not eat - forced empty impl */ }
public void sleep() { /* Robots do not sleep - forced empty impl */ }
}
4.2. ISP Correct Implementation
The best place to look for IPS examples is Java AWT event handlers for handling GUI events fired from keyboard and mouse. It has different listener classes for each kind of event. We only need to write handlers for events, we wish to handle. Nothing is mandatory.
Some of the listeners are –
- FocusListener
- KeyListener
- MouseMotionListener
- MouseWheelListener
- TextListener
- WindowFocusListener
By splitting the interface, we give each implementing class the freedom to pick only what it needs. We keep our interfaces focused and our implementations honest.
public class MouseMotionListenerImpl implements MouseMotionListener
{
@Override
public void mouseDragged(MouseEvent e) {
//handler code
}
@Override
public void mouseMoved(MouseEvent e) {
//handler code
}
}
A good rule of thumb: if we find ourselves writing empty method bodies or throwing UnsupportedOperationException to satisfy an interface, that interface probably needs to be split.
5. Dependency Inversion Principle (DIP)
Most of us are already familiar with the words used in the principle name. DI principle says:
“High-level modules should not depend on low-level modules. Both should depend on abstractions.“
Additionally, abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
In other words. we should design our software so that various modules can be separated from each other using an abstract layer to bind them together.
5.1. DIP Violation Example
In the following example, if we need to switch from MySQL to PostgreSQL, we must modify UserRepository. It also becomes impossible to write unit tests without spinning up a real database connection.
// BAD: High-level class directly depends on a concrete low-level class
public class MySQLConnection {
public void connect() {
System.out.println("Connecting to MySQL database...");
}
}
public class UserRepository {
private MySQLConnection connection;
public UserRepository() {
// Hard dependency on a concrete implementation
this.connection = new MySQLConnection();
}
public void saveUser(String user) {
connection.connect();
System.out.println("Saving: " + user);
}
}
5.2. DIP Correct Implementation
In the correct implementation below, UserRepository depends only on the DatabaseConnection abstraction. We can swap the database implementation at any time without changing the repository logic at all. We can also inject a mock in tests.
// GOOD: Depend on an abstraction, not a concrete class
public interface DatabaseConnection {
void connect();
}
public class MySQLConnection implements DatabaseConnection {
public void connect() {
System.out.println("Connecting to MySQL...");
}
}
public class PostgreSQLConnection implements DatabaseConnection {
public void connect() {
System.out.println("Connecting to PostgreSQL...");
}
}
public class UserRepository {
private DatabaseConnection connection;
// Dependency is injected through the constructor
public UserRepository(DatabaseConnection connection) {
this.connection = connection;
}
public void saveUser(String user) {
connection.connect();
System.out.println("Saving: " + user);
}
}
// Usage
DatabaseConnection db = new PostgreSQLConnection();
UserRepository repo = new UserRepository(db);
repo.saveUser("Alice");
...
...
DIP is the foundation of Dependency Injection (DI) frameworks like Spring. When we use @Autowired or constructor injection in Spring Boot, we are applying this principle in practice every day.
These separate components are so well closed in their boundaries that we can use them in other software modules apart from spring with the same ease. This has been achieved by dependency inversion and open-closed principles. All modules expose only abstraction, which is useful in extending the functionality or plug-in in another module.
6. Benefits of Following SOLID Principles in Java
After we apply these principles consistently across our codebase, we start noticing several tangible improvements in our development workflow:
- Easier unit testing because classes have focused responsibilities and dependencies are injected rather than hardcoded.
- Reduced risk of regression, since changes to one part of the system are less likely to break unrelated parts.
- Faster onboarding for new team members, because well-structured code is far easier to read and reason about.
- Better scalability, as extending the application requires writing new classes rather than refactoring existing ones.
- Lower technical debt over time, reducing maintenance cost across the entire software lifecycle.
7. Frequently Asked Questions
7.1. Are SOLID principles only for Java?
No. While the examples in this guide are in Java, SOLID principles apply to any object-oriented language including C#, Python, Kotlin, and TypeScript. The underlying design thinking is universal.
7.2. Do we need to follow all five principles in every project?
Not necessarily. These are guidelines, not mandates. In smaller scripts or throwaway code, strict adherence may add unnecessary complexity. The value of SOLID becomes most apparent in medium-to-large applications that evolve over time.
7.3. How does SOLID relate to Design Patterns?
SOLID principles and design patterns complement each other. Many classic design patterns from the Gang of Four, such as Strategy, Factory, and Decorator, are essentially concrete implementations of one or more SOLID principles. Understanding SOLID makes it much easier to understand why those patterns work the way they do.
7.4. What is the relationship between DIP and Spring Dependency Injection?
Spring’s dependency injection framework is a direct application of the Dependency Inversion Principle. When we wire beans through constructor injection or @Autowired, Spring manages the creation of concrete objects and injects them through abstractions, exactly as DIP recommends.
8. Conclusion
These were 5 class design principles, also known as SOLID principles, which make the best practices to be followed to design our application classes and interfaces. When followed carefully, our systems design is easy to maintain and change overtime.
Happy Learning !!
I think last one ‘Dependency Inversion Principle’ explanation seems not correct. This one in not related to “Dependency Injection”.
Based on this idea, Robert C. Martin’s definition of the Dependency Inversion Principle consists of two parts:
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.
C can replace A and B. This is the concept.
I have questions about the principle: Liskov Substitution Principle Example.
In some cases, I have Class A and Class B. They have some repeat codes, I extract these codes to a base abstract Class C and then Class A and B extend C, then the repeate codes can be reused. I think the method violate the Subsititution Principle because Class A and B can not substituted by C. What do you think and what else I can do to handle these repeat codes.
Thanks
I do not see any violation here.
Just a suggestion why can’t you use factory pattern instead of abstract class if possible as per solid design pattern i don’t see any problem but use of abstract classes should be avoided
Thank you for the simple explanation with necessary points to understand the concept.
Would suggest try to have some uml diagram along with pictorial representation . Also some code examples as well.
well explained here with examples https://blogs.oracle.com/java/post/core-design-principles .
All this concept used to given by those who want to become famous only with Articles and use to confuse others
There are few thumb rule
1. Re usability
2. Upgradibility with Zero or minimal downtime
3. Scalability
4. Manageability
5. Loose coupling
Keep in mind these thumb rule automatically you will design best
Talk about something you do everyday, otherwise just read and leave the place instead of criticising.
I think the rules Siddharth Singh mentioned like software designing result. The SOLID principles more like methods or some lessons can lead us to the result.
HI Lokesh, in the summary image (https://howtodoinjava.com/wp-content/uploads/solid_class_design_principles.png), there is a spelling mistake under What is says. “Reasonability” should be replaced by “responsibility”
Good Explanation!
But unlike your other posts this just seems theoretical, it would really help if you can include a problem statement and show the design step by step using these principles. Additionally, you may also show what problems we can face when we do not follow or how do we implement in the wrong way.
Keep up the good work. This site is awesome!
Thank you Lokesh.
Nice write-up, need more detailed on Open & Closed principle.
Liskov Substitution Principle —> (depends on ) —-> Open for extension but closed for modification — > (depends on) —> Dependency Inversion Principle.
All these rules are inter-related if you use it wisely, you won’t find any problem.
Let us take an example:
1. Suppose a requirement comes where we need to have a Class car, with various operations like calculating top speed, mileage, insurance, taxes.
2. Now in future, requirement comes where cars can be of luxury and non luxury, so in each method we start adding if and else clause to calculate insurance, taxes, top speed for both types.
3. Now in future, when requirement for sports cars have also come.
You think of refactoring the code.
1. You would separate out Car class as abstract. [Following Dependency Inversion Principle]
2. Luxury, Non Luxury, Super Cars would extend and override the methods. [Subsequently, Follow Liskov Substitution Principle ]
You might argue that you could have started refactoring at point 2 of requirement itself, that is true but I wanted to show how difficult it can become, if you don’t refactor as soon as you feel it is not following Open Closed principle.
Summary : If you follow Dependency Inversion Principle you are making sure Open Closed Principle would work, and if you follow that you are making sure Liskov Substitution Principle works.
P.S : I am not generalizing, it is just that most of the time everything will fall in place, If you follow like this.
That ‘s really useful. Great works!
Awesome explanation!
You done great job Lokesh !!!
Please take a look : https://en.wikipedia.org/wiki/SOLID_%28object-oriented_design%29
Good job.
Would be great if each had a brief scenario/example
Hi Lokesh Gupta,
This is a good read no doubt! however, shouldn’t we mention UncleBob for S.O.L.I.D.
Regards,
Vijay Jayaram
I have never heard of him. Did he originally wrote these principles?
sarcasm
I think No. these are 5 SOLID principals used or taking care during software designing.
Please, Explain with example …
vivid clear description..thank you :)
“For example, if you take a look into any good framework like struts or spring, you will see that you can change their core logic and request processing, BUT you modify the desired application flow just by extending some classes and plugin them in configuration files.”
Could not understand what you wanted to convey from first part of the sentence.
oops, typo. Corrected. It is “can not change their core logic”. Thanks for bringing into notice.
Thanks for the quick reply! Keep up the great work. Cheers!
nice post.. can you please explain with some real world examples..
nice post
I think OCP is explained wrong would be more clear with example code.
Which sentence you think is wrong? I will try to explain in more detail.
Good stuff… can you also explain Dependency Injection in detail?
Good one!
Thanks !! for sharing great stuff..
If some sample code are there it will be more beneficial and easy to understand.
This one is good for guide, but this article more useful with sample codes
It would be better, if you give small examples with SOLID.