SOLID is a group of five object-oriented design principles. They help us decide what belongs in a class, how classes depend on one another and how we can add behavior without making unrelated code harder to change.
The five principles are Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation and Dependency Inversion. They give us questions to ask about a design, rather than a requirement to add interfaces and inheritance everywhere.
This guide explains each principle with a small Java example. We first look at the design decision, then the code and what changes when we apply the principle. The examples illustrate separate designs and occasionally reuse a data record. They do not form one application. They use Java 17 or later. A record is a compact data carrier that supplies a constructor and accessors such as name() automatically.
1. SOLID Principles at a Glance
| Principle | Main question | A common warning sign |
|---|---|---|
| Single Responsibility | Do the responsibilities in this class change for the same reason? | Email changes require edits to a storage class |
| Open-Closed | Can we add an expected variation without editing its stable caller? | Every report format adds another condition to one generator |
| Liskov Substitution | Does an implementation keep the promises of its type? | Replacing one implementation breaks valid caller behavior |
| Interface Segregation | Does this client depend on operations it does not need? | A read-only client must depend on write operations too |
| Dependency Inversion | Does business logic depend on a useful contract or an implementation detail? | An order service constructs its own database adapter |
The principles overlap, but they ask different questions. A small interface can support several of them, yet its presence alone does not prove that a design is good.
2. Single Responsibility Principle
The single responsibility principle, or SRP, asks us to group behavior that changes for the same reason. A responsibility can include several related methods.
For example, an employee store can add, find and update employee records. These methods belong together because they support storage. Sending welcome emails introduces a separate responsibility with different requirements.
interface EmployeeStore {
void add(Employee employee);
Employee findById(long id);
}
interface WelcomeEmailSender {
void sendTo(Employee employee);
}
record Employee(long id, String name, String email) {}
final class EmployeeRegistration {
private final EmployeeStore employees;
private final WelcomeEmailSender emails;
EmployeeRegistration(EmployeeStore employees, WelcomeEmailSender emails) {
this.employees = employees;
this.emails = emails;
}
void register(Employee employee) {
employees.add(employee);
emails.sendTo(employee);
}
}
EmployeeStore describes storage. WelcomeEmailSender describes delivery. EmployeeRegistration coordinates the registration operation without containing either implementation.
If we change email delivery, we can usually keep that change away from storage code. A change to registration policy may still affect the coordinator. Separating responsibilities reduces unrelated changes; it does not guarantee that a feature touches only one file.
We also need a policy for delivery failures after an employee has been saved. SRP does not create a transaction across the database and an email service. The dedicated SRP guide explains this separation and its limits.
3. Open-Closed Principle
The open-closed principle, or OCP, encourages us to make useful variations possible without repeatedly editing stable behavior. We choose a contract for the part that varies and let the caller use that contract.
An area calculator that checks whether an object is a circle or a rectangle needs another condition for each new shape. Instead, we can give every shape an area() operation.
interface Shape {
double area();
}
record Circle(double radius) implements Shape {
@Override
public double area() {
return Math.PI * radius * radius;
}
}
record Rectangle(double width, double height) implements Shape {
@Override
public double area() {
return width * height;
}
}
final class AreaCalculator {
double totalArea(java.util.List<Shape> shapes) {
return shapes.stream().mapToDouble(Shape::area).sum();
}
}
These small examples assume valid, non-negative dimensions. The calculator asks each shape for its area and adds the results. It does not need to know the formula for each shape.
We can add a Triangle that implements Shape without adding another condition to totalArea(). Code that constructs the collection still has to include the new shape. Also, a new requirement that changes what “area” means may require us to change the contract itself.
OCP does not mean that we must never modify a class or always use inheritance. The report writer example shows how composition provides an extension point.
4. Liskov Substitution Principle
The Liskov substitution principle, or LSP, says that we should be able to use an implementation wherever its declared type is expected without breaking that type’s promised behavior. The promise includes what inputs are accepted, what results mean and what state changes are allowed.
The Java compiler checks method signatures, but matching signatures is not enough. A method can compile and still behave in a way its callers cannot safely use.
4.1. Why a Mutable Square Can Break a Rectangle Contract
Consider a mutable rectangle whose setWidth() method changes only its width and whose setHeight() method changes only its height. A caller can reasonably set the width to 5, set the height to 10 and expect an area of 50.
If a Square subclass overrides both setters to keep its sides equal, setting the height to 10 also changes the width to 10. The same caller now receives an area of 100.
The problem is the setter contract, not the mathematical relationship between squares and rectangles. The square cannot preserve that rectangle API’s promise of independently changeable dimensions.
4.2. Use a Contract Both Types Can Keep
We can instead describe the behavior both types genuinely provide, such as reporting an area. In this separate example, the shapes are immutable, so there is no promise about independently changing dimensions.
interface AreaShape {
double area();
}
record FixedRectangle(double width, double height) implements AreaShape {
@Override
public double area() {
return width * height;
}
}
record FixedSquare(double side) implements AreaShape {
@Override
public double area() {
return side * side;
}
}
Again, we assume valid dimensions. A caller that only needs area() can use either type. We have chosen a smaller promise that both can keep.
When reviewing LSP, we check documented behavior as well as types. Does an implementation reject input that the contract allows? Does it change state in a way callers were told would not happen? Does a normally supported operation suddenly throw an unsupported-operation exception?
An optional operation documented by an API is different from an implementation unexpectedly breaking a mandatory operation. The contract determines what substitution means.
5. Interface Segregation Principle
The interface segregation principle, or ISP, asks us to give clients the operations they need without making them depend on unrelated operations.
A reporting screen may only read employee data. An administration service may also create and delete employees. Giving both clients the same large interface can make the reporting screen depend on changes to write operations it never uses.
We can separate the read and write contracts. Each caller then depends on the operations it uses.
interface EmployeeReader {
Employee findById(long id);
}
interface EmployeeWriter {
void save(Employee employee);
void deleteById(long id);
}
final class EmployeeReport {
private final EmployeeReader employees;
EmployeeReport(EmployeeReader employees) {
this.employees = employees;
}
String employeeName(long id) {
return employees.findById(id).name();
}
}
Here, Employee has the fields shown in the earlier employee example, and findById() is assumed to return an existing employee or throw an appropriate exception. The report only depends on EmployeeReader. One database adapter can implement both interfaces without exposing both to every caller.
ISP does not require one method per interface. A set of related reading operations may form a useful interface. We split it when different clients have meaningfully different needs, rather than splitting solely to make interfaces shorter.
6. Dependency Inversion Principle
The dependency inversion principle, or DIP, asks us to keep business policy from depending directly on low-level implementation details. Both sides depend on a contract that expresses what the policy needs.
For example, an order service needs to store an order. It does not need to construct a PostgreSQL connection or know how a particular database driver works. We can express the storage operation through OrderStore.
record Order(long id) {}
interface OrderStore {
void save(Order order);
}
final class OrderService {
private final OrderStore orders;
OrderService(OrderStore orders) {
this.orders = orders;
}
void place(Order order) {
orders.save(order);
}
}
The application defines OrderStore. A database adapter implements it and contains the database-specific code. At startup, we construct that adapter and pass it to OrderService. The service depends on the contract, while the adapter also follows that contract. The contract expresses an application operation, rather than exposing details such as opening a database connection.
6.1. DIP and Dependency Injection Are Different
Dependency injection means supplying an object’s dependencies from outside it. The constructor above is one way to do that. DIP is the design choice about what those dependencies are and which direction the code depends on them.
We can inject a concrete database class and still make our business code depend on that class. Injection alone does not establish DIP. We also do not need Spring to use constructor injection. The Spring dependency injection documentation explains how a container can perform the wiring when we use Spring.
7. Applying SOLID Without Adding Unnecessary Complexity
We start with the code and the changes it needs to support. A class that is easy to understand and has no unrelated responsibilities does not need to be divided simply because it is longer than another class.
Similarly, an interface earns its place when it expresses a useful boundary or variation. Creating an interface for every class can add files without making a change easier. A small application can use these principles with a few clear classes.
Tests help check the promises we choose. A unit test can exercise a business rule with a controlled dependency, while a database test checks the real storage adapter. Neither test proves the entire application by itself. The unit testing guide explains how these checks fit together.
8. Frequently Asked Questions
8.1. Do SOLID Principles Make an Application Faster?
They mainly address how code is organized and changed. They do not guarantee lower response times or higher throughput. We measure performance separately and consider the cost of additional indirection when it matters.
8.2. Are SOLID Principles Only for Java?
No. These ideas apply to object-oriented designs in other languages too. The language’s features may change how we express the design, so the same principle does not always lead to the same code structure.
8.3. What Is the Difference Between a Principle and a Design Pattern?
A principle helps us evaluate a design decision. A pattern describes a reusable arrangement of collaborating objects. For example, supplying a strategy object can help apply OCP, but using that pattern does not automatically make every surrounding design decision appropriate.
9. Conclusion
SOLID helps us examine responsibilities, extension points, behavioral promises, interfaces and dependency direction. We use those questions to make changes clearer and more contained. The goal is a design we can understand and work with, rather than the largest possible collection of abstractions.
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.