Learn SOLID Design Principles in Java by Coding It
An in-depth explanation of all SOLID Design Principles with real-world use cases and code examples

Table of Contents
- What is SOLID and why you should bother using it
- Single Responsibility Principle
- Open Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
In this article, we will be discussing the SOLID design principles. First, we will understand why they came out, and then we will understand how to implement each principle with code examples.
1. What is SOLID and why you should bother using it
In his paper Design Principles and Design Patterns, Robert C. Martin introduced the principles, and later on, Michael Feathers introduced that acronym.
Design principles in general encourage us to write better software, more maintainable, understandable, and flexible. It also improves the developer experience of those who are going to be part of your team in the future.
S.O.L.I.D stands for:
- Single Responsibility Principle
- Open Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
2. Single Responsibility Principle
This principle states that a class should have only one responsibility, in other words, a class should have only one reason to change.
It means that only when there is a change on the functionality that our class has, it should change.
You can have benefits, such as, onboarding new members will be easier, testing will be easier, and so on.
Many frameworks and libraries follow this principle. For example, CrudRepository from Spring Data, the Validation API, the Date/Time API.
Use Case
Imagine that we have a class ProductService that has two concerns and responsibilities:
- Manipulating crud operations on a product.
- Sending SMS and EMAIL notifications based on crud operations.
SingleResponsibility.java
What happens if the requirements of this class change, and now we have a text email to be sent and an HTML email to be sent that demand different implementations of the sendEmail()
method ?
And then later on we can have a different product manipulation requirement that also demands changes.
Following this, the ProductService
class changes based on notification related reasons and product related reasons.
It would be better to separate those concerns:
SingleResponsibility.java
Even though we are creating different methods on each requirement change, the idea is to separate concerns here, the solutions to solve this kind of problem is a whole different topic.
Common Discussions
Every engineer/team has their own idea of a reason to change. There is no strict idea to follow when it comes to deciding a class purpose. It all depends on your own business rule, your project, your team, etc.
The key is not to overthink. Try to think of a single responsibility being, even if methods perform different operations, do they operate on the same purpose?
3. Open Closed Principle
This principle states that software entities (classes, modules, functions, etc.) should be opened to extension but closed to modification.
It means that you should avoid having to modify the logic of something on your system when the components are growing. It is easier to visualize in a real example.
Use Case
Imagine that we have the famous problem of calculating the area of geometric shapes.
AreaCalculator.java
Even with a basic example, it can be boring having to change AreaCalculator
each time a new shape area needs to be calculated, with a new method because the logic changes each time.
We can make use of simple abstraction and polymorphism to handle this.
AreaCalculator.java
That way, AreaCalculator
won’t know which and how many shapes there are to handle, and its own implementation.
Now that class is opened for extension (Triangle
, Circle
, Rectangle
, etc.) and closed for modification(won’t have a method with a different logic being added every time a new shape comes up).
4. Liskov Substitution Principle
This principle states substitutability of a class by its subclass, so a class can be replaced by its subclass in all practical usage scenarios, meaning that you should use inheritance only for substitutability.
“Subtypes must be substitutable for their base types.”
— Robert C. Martin
“If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.”
— Barbara Liskov
In a nutshell:
A ChildClass
should only extend a ParentClass
if we can replace a ParentClass
object by a ChildClass
object without changing the behavior of the program, otherwise we should use Composition or Delegation.
Use Case
Imagine we have a parent class Bird
. We can have many child classes, such as Sparrow, Ostrich, Eagle, Falcon, etc.
Is it right to have a Sparrow
and an Ostrich
class, extending a Bird
class ? Following the Liskov Substitution Principle, it isn’t.
Even though an Ostrich is also a Bird, it doesn’t make sense to have its object being able to fly, because it can’t.
We could break up the inheritance into a smaller level to follow this principle.
That way, the Bird class won’t be replaced in the wrong way by a Sparrow class in any scenario.
5. Interface Segregation Principle
This principle states that a client shouldn’t be forced to implement an interface that it doesn’t use.
It is kind of like the Single Responsibility Principle, but at an interface level.
Use Case
Imagine if we have an interface Worker with two methods, work() and sleep(). That way, every concrete worker class will be able to work and sleep.
Does it make sense to have the Robot worker implementing the sleep() method even though we know it can’t ?
We can solve this by breaking up the interface into smaller and more specific interfaces.
That way, we can reduce the side effects of using larger and general interfaces, and have each interface serving a single purpose.
6. Dependency Inversion Principle
“High-level modules should not depend on low-level modules. Both should depend on abstractions.”
— Robert C. Martin
“Abstractions should not depend on details. Details should depend on abstractions.”
— Robert C. Martin
This principle states that we should invert the classic dependency between higher level modules and lower level modules, by abstracting their interaction.
It splits the dependency between the high level and low level modules, by introducing an interface or abstract class between them.
At the end of the day, you end up with a high level module that depends on an abstraction and a low level module that depends on an abstraction.
Use Case
UML Diagram

Our high-level would be CustomerService, our low-level would be MySqlImpl and PostgreSqlImpl and our abstraction would be CustomerRepository.
CustomerService (high-level)
CustomerRepository (abstraction)
MySqlImpl (low-level)
PostgreSqlImpl (low-level)
That way, any implementation of CustomerRepository you use with CustomerService will be independent of a unique database for example, in case you need to change to SqlServer eventually, you will depend on having another implementation, without your high-level CustomerService knowing what’s going on.
All code related to this article can be found at Project GitHub Repo. Feel free to contribute on GitHub in any way as well, contributions are more than welcome.
References
- Head First Design Patterns, Kathy Sierra and Elisabeth Robson
- Design Patterns in Java, William C. Wake
- Design Principles and Design Patterns, Robert C. Martin
- Clean Architecture, Tom Hombergs