SOLID Principles
Writing code that works is just the beginning. To truly build scalable and long-lasting software, your code must be clean, modular, and easy to maintain.
That’s where the SOLID principles come in—a set of five design guidelines introduced by Robert C. Martin (Uncle Bob) in the early 2000s. These principles help developers write flexible, decoupled code that’s easier to evolve and debug.
In this post, we’ll break down each of the SOLID principles with practical explanations and real-world examples.
🧱 S – Single Responsibility Principle (SRP)
A class should have one, and only one, reason to change.
Every class in your code should be focused on doing one job well. When a class tries to handle too many responsibilities, it becomes a tangled mess. Changes in one part might break unrelated functionality, leading to unexpected bugs.
❌ Example: Breaking SRP
class UserManager {
void authenticate(String username, String password) { ... }
void updateProfile(User user) { ... }
void sendWelcomeEmail(User user) { ... }
}
This class manages authentication, profile updates, and email notifications—three separate concerns.
✅ Refactored with SRP
class AuthService {
void authenticate(String username, String password) { ... }
}
class ProfileService {
void updateProfile(User user) { ... }
}
class NotificationService {
void sendWelcomeEmail(User user) { ... }
}
Now, each class has a single focus, making them easier to test, change, and extend.
🧩 O – Open/Closed Principle (OCP)
Software should be open for extension but closed for modification.
Your code should allow new features to be added without changing existing code. This reduces the risk of introducing bugs when requirements change.
❌ Example: Violating OCP
class ShapeCalculator {
double calculateArea(Object shape) {
if (shape instanceof Circle) { ... }
else if (shape instanceof Rectangle) { ... }
}
}
Every time you add a new shape, you need to modify the calculator logic.
✅ Refactored with OCP
interface Shape {
double area();
}
class Circle implements Shape {
public double area() { ... }
}
class Rectangle implements Shape {
public double area() { ... }
}
class ShapeCalculator {
double calculateArea(Shape shape) {
return shape.area();
}
}
Now, new shapes can be added without touching the calculator logic.
🔁 L – Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types without altering the correctness of the program.
If a class B is a subclass of A, you should be able to use B anywhere A is expected, without causing issues.
❌ Bad Example
class Vehicle {
void startEngine() { ... }
}
class Bicycle extends Vehicle {
@Override
void startEngine() {
throw new UnsupportedOperationException();
}
}
Bicycles don’t have engines, so this design breaks LSP.
✅ Refactored with LSP
abstract class Vehicle {
abstract void start();
}
class Car extends Vehicle {
void start() { /* start engine */ }
}
class Bicycle extends Vehicle {
void start() { /* start pedaling */ }
}
Now both subclasses follow the contract of the base class without introducing surprises.
🎛️ I – Interface Segregation Principle (ISP)
Clients shouldn’t be forced to depend on interfaces they don’t use.
It’s better to have multiple small, focused interfaces rather than one large one that tries to do everything.
❌ Bloated Interface
interface MediaPlayer {
void playAudio();
void playVideo();
void adjustVideoSettings();
}
An audio-only player would still need to implement video-related methods.
✅ Refactored with ISP
interface AudioPlayer {
void playAudio();
}
interface VideoPlayer {
void playVideo();
void adjustVideoSettings();
}
class MP3Player implements AudioPlayer {
void playAudio() { ... }
}
class VLCPlayer implements AudioPlayer, VideoPlayer {
void playAudio() { ... }
void playVideo() { ... }
void adjustVideoSettings() { ... }
}
Each class now implements only the functionality it needs.
🔌 D – Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Tightly coupling classes to specific implementations makes code harder to maintain. Instead, depend on interfaces or abstractions.
❌ Bad Dependency
class GmailClient {
void send(String message) { ... }
}
class EmailService {
GmailClient client = new GmailClient();
void sendEmail(String message) {
client.send(message);
}
}
✅ Refactored with DIP
interface EmailClient {
void send(String message);
}
class GmailClient implements EmailClient {
public void send(String message) { ... }
}
class EmailService {
private final EmailClient client;
EmailService(EmailClient client) {
this.client = client;
}
void sendEmail(String message) {
client.send(message);
}
}
Now EmailService is flexible and testable—it can work with any email client implementation.