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.

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
}
}
}
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.

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;
}
}
}
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

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
}
}
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.

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
}
}
}
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.

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);
}
}
}
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)
Git Repo