SOLID Principles Explained With Examples
Modern software development is not only about writing code that works — it is about writing code that is maintainable, scalable, reusable, and easy to understand. As applications grow larger, poorly structured code quickly becomes difficult to manage. This is where the SOLID principles become extremely important.
SOLID is a collection of five object-oriented design principles introduced by Robert C. Martin, widely known as “Uncle Bob.” These principles help developers create software systems that are easier to maintain, extend, and test.
The acronym 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)
In this blog, we will explain each principle in detail with simple examples using object-oriented programming concepts.
Why SOLID Principles Matter
Before diving into each principle, it is important to understand why SOLID matters.
Without proper design principles, software often suffers from:
- Tight coupling
- Repeated code
- Difficult debugging
- Hard-to-test modules
- Poor scalability
- Fragile architecture
SOLID principles help solve these issues by encouraging:
- Clean architecture
- Flexible design
- Reusable components
- Easier testing
- Better collaboration among developers
These principles are heavily used in frameworks like Spring Framework, ASP.NET Core, and Laravel.
1. Single Responsibility Principle (SRP)
Definition
A class should have only one reason to change.
This means a class should handle only one responsibility or functionality.
Bad Example
class Report {
public void generateReport() {
System.out.println("Generating report...");
}
public void saveToFile() {
System.out.println("Saving report to file...");
}
public void sendEmail() {
System.out.println("Sending email...");
}
}
Problem
The Report class is handling:
- Report generation
- File storage
- Email sending
If email logic changes, the class changes.
If file storage changes, the same class changes again.
This violates SRP.
Good Example
class ReportGenerator {
public void generateReport() {
System.out.println("Generating report...");
}
}
class ReportSaver {
public void saveToFile() {
System.out.println("Saving report...");
}
}
class EmailSender {
public void sendEmail() {
System.out.println("Sending email...");
}
}
Benefits
- Easier maintenance
- Better readability
- Independent testing
- Reduced complexity
2. Open/Closed Principle (OCP)
Definition
Software entities should be:
- Open for extension
- Closed for modification
This means you should add new functionality without changing existing code.
Bad Example
class PaymentProcessor {
public void processPayment(String type) {
if(type.equals("CreditCard")) {
System.out.println("Processing credit card payment");
}
else if(type.equals("PayPal")) {
System.out.println("Processing PayPal payment");
}
}
}
Problem
Whenever a new payment method is added, you must modify existing code.
This increases risk and breaks stability.
Good Example
interface PaymentMethod {
void pay();
}
class CreditCardPayment implements PaymentMethod {
public void pay() {
System.out.println("Credit card payment");
}
}
class PayPalPayment implements PaymentMethod {
public void pay() {
System.out.println("PayPal payment");
}
}
class PaymentProcessor {
public void processPayment(PaymentMethod method) {
method.pay();
}
}
Now you can add new payment methods without changing PaymentProcessor.
Example:
class UpiPayment implements PaymentMethod {
public void pay() {
System.out.println("UPI Payment");
}
}
Benefits
- Safer code changes
- Better scalability
- Easier feature additions
3. Liskov Substitution Principle (LSP)
Definition
Objects of a subclass should be replaceable with objects of the parent class without breaking the application.
In simple words:
A child class should behave like its parent class.
Bad Example
class Bird {
public void fly() {
System.out.println("Bird can fly");
}
}
class Ostrich extends Bird {
public void fly() {
throw new UnsupportedOperationException();
}
}
Problem
An ostrich cannot fly, but it inherits fly().
This breaks LSP because replacing Bird with Ostrich causes unexpected behavior.
Good Example
class Bird {
}
class FlyingBird extends Bird {
public void fly() {
System.out.println("Flying...");
}
}
class Sparrow extends FlyingBird {
}
class Ostrich extends Bird {
}
Now only flying birds have flying behavior.
Benefits
- Better inheritance structure
- Fewer runtime errors
- More predictable behavior
4. Interface Segregation Principle (ISP)
Definition
Clients should not be forced to implement interfaces they do not use.
Instead of large interfaces, create smaller and focused interfaces.
Bad Example
interface Worker {
void work();
void eat();
}
class Robot implements Worker {
public void work() {
System.out.println("Robot working");
}
public void eat() {
// Robots don't eat
}
}
Problem
Robot is forced to implement eat() even though it does not need it.
Good Example
interface Workable {
void work();
}
interface Eatable {
void eat();
}
class Human implements Workable, Eatable {
public void work() {
System.out.println("Human working");
}
public void eat() {
System.out.println("Human eating");
}
}
class Robot implements Workable {
public void work() {
System.out.println("Robot working");
}
}
Benefits
- Cleaner interfaces
- Less unnecessary code
- Improved flexibility
5. Dependency Inversion Principle (DIP)
Definition
High-level modules should not depend on low-level modules.
Both should depend on abstractions.
In simple words:
Depend on interfaces, not concrete classes.
Bad Example
class Keyboard {
public void type() {
System.out.println("Typing...");
}
}
class Computer {
private Keyboard keyboard = new Keyboard();
public void start() {
keyboard.type();
}
}
Problem
Computer is tightly coupled to Keyboard.
Changing the keyboard implementation requires modifying Computer.
Good Example
interface InputDevice {
void input();
}
class Keyboard implements InputDevice {
public void input() {
System.out.println("Typing...");
}
}
class Mouse implements InputDevice {
public void input() {
System.out.println("Clicking...");
}
}
class Computer {
private InputDevice device;
public Computer(InputDevice device) {
this.device = device;
}
public void start() {
device.input();
}
}
Now Computer works with any input device.
Benefits
- Loose coupling
- Better testability
- Easier dependency injection
- More flexible architecture
Real-World Example of SOLID Principles
Consider an e-commerce application.
Without SOLID:
- One giant class handles orders, payments, emails, and reports.
- Adding new payment methods becomes difficult.
- Testing individual modules becomes almost impossible.
With SOLID:
- Separate services handle separate responsibilities.
- Payment methods become extensible.
- Interfaces isolate features.
- Dependencies become loosely coupled.
This results in:
- Faster development
- Easier debugging
- Cleaner architecture
- Better scalability
SOLID Principles in Popular Frameworks
Many modern frameworks internally follow SOLID principles.
Java
- Spring Boot uses dependency injection heavily.
- Interfaces and abstractions are core concepts.
C
- ASP.NET promotes dependency injection and interface-based architecture.
PHP
- Laravel uses service containers and inversion of control.
Swift
- SwiftUI encourages modular and reusable design patterns.
Common Mistakes Developers Make
Even experienced developers sometimes misuse SOLID principles.
Overengineering
Some developers create too many abstractions for small applications.
SOLID should simplify code, not make it unnecessarily complicated.
Misusing Inheritance
Inheritance should represent true “is-a” relationships.
Example:
- Car is a Vehicle ✅
- Car is an Engine ❌
Ignoring Interfaces
Hardcoding dependencies makes testing difficult.
Interfaces improve flexibility and mock testing.
Tips to Apply SOLID Principles
Start Small
You do not need to redesign your entire application immediately.
Apply SOLID gradually.
Use Composition Over Inheritance
Prefer combining objects rather than deeply nested inheritance trees.
Write Unit Tests
SOLID works best with testing practices.
Frameworks like:
- JUnit
- XCTest
- Mockito
help validate modular code design.
Advantages of SOLID Principles
| Principle | Main Benefit |
| | — |
| SRP | Easier maintenance |
| OCP | Easy extension |
| LSP | Reliable inheritance |
| ISP | Smaller interfaces |
| DIP | Loose coupling |
Overall advantages:
- Better architecture
- Cleaner code
- Improved readability
- Easier testing
- Faster debugging
- Scalable systems
When Should You Use SOLID?
SOLID principles are especially useful for:
- Enterprise applications
- Backend systems
- APIs
- SaaS products
- Mobile apps
- Large collaborative projects
For very small scripts or prototypes, applying every principle strictly may not be necessary.
Conclusion
SOLID principles are foundational concepts in object-oriented software development. They help developers create applications that are flexible, maintainable, scalable, and easier to understand.
To summarize:
- SRP keeps classes focused
- OCP allows safe extension
- LSP ensures proper inheritance
- ISP avoids bloated interfaces
- DIP reduces tight coupling
Mastering SOLID principles can significantly improve code quality and software architecture. Whether you are building APIs, mobile applications, SaaS platforms, or enterprise systems, these principles help you write professional and future-proof code.
Instead of only writing code that works today, SOLID encourages writing code that remains manageable tomorrow.
