Introduction
Designing robust and maintainable software in Java requires more than just syntax and features. It demands principles that guide your architecture, ensure flexibility, and promote testability.
Enter SOLID principles—a set of five guidelines that form the foundation of clean object-oriented programming.
- S – Single Responsibility Principle
- O – Open/Closed Principle
- L – Liskov Substitution Principle
- I – Interface Segregation Principle
- D – Dependency Inversion Principle
This guide will help you understand and apply each principle with clear Java examples, diagrams, and real-world use cases.
What Are SOLID Principles?
SOLID is an acronym coined by Robert C. Martin (Uncle Bob) that represents five design principles intended to improve software structure and maintainability.
They are especially relevant when working with Java OOP in enterprise or modular applications.
S – Single Responsibility Principle (SRP)
Definition
A class should have only one reason to change.
Java Example – Bad
class Invoice {
void calculateTotal() {}
void printInvoice() {} // violates SRP
void saveToDatabase() {} // violates SRP
}
✅ Refactored
class Invoice {
void calculateTotal() {}
}
class InvoicePrinter {
void print(Invoice invoice) {}
}
class InvoiceRepository {
void save(Invoice invoice) {}
}
UML-style
Invoice ➝ InvoicePrinter
➝ InvoiceRepository
O – Open/Closed Principle (OCP)
Definition
Software entities should be open for extension but closed for modification.
Java Example – Bad
class DiscountCalculator {
double calculate(String customerType) {
if (customerType.equals("Regular")) return 10;
if (customerType.equals("Premium")) return 20;
return 0;
}
}
✅ Refactored (OCP)
interface DiscountStrategy {
double calculate();
}
class RegularDiscount implements DiscountStrategy {
public double calculate() { return 10; }
}
class PremiumDiscount implements DiscountStrategy {
public double calculate() { return 20; }
}
class DiscountCalculator {
public double calculate(DiscountStrategy strategy) {
return strategy.calculate();
}
}
L – Liskov Substitution Principle (LSP)
Definition
Subtypes must be substitutable for their base types without altering program correctness.
Java Example – Violation
class Bird {
void fly() {}
}
class Ostrich extends Bird {
void fly() {
throw new UnsupportedOperationException();
}
}
✅ Fix
interface Bird {}
interface FlyingBird extends Bird {
void fly();
}
class Sparrow implements FlyingBird {
public void fly() {}
}
class Ostrich implements Bird {
// no fly()
}
I – Interface Segregation Principle (ISP)
Definition
Clients should not be forced to depend on interfaces they do not use.
Java Example – Bad
interface Worker {
void work();
void eat();
}
Robots can work but not eat—this violates ISP.
✅ Refactored
interface Workable {
void work();
}
interface Eatable {
void eat();
}
class Human implements Workable, Eatable {
public void work() {}
public void eat() {}
}
class Robot implements Workable {
public void work() {}
}
D – Dependency Inversion Principle (DIP)
Definition
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Java Example – Bad
class MySQLDatabase {
void save(String data) {}
}
class UserService {
private MySQLDatabase db = new MySQLDatabase();
void saveUser(String data) {
db.save(data);
}
}
✅ Refactored with DIP
interface Database {
void save(String data);
}
class MySQLDatabase implements Database {
public void save(String data) {}
}
class UserService {
private Database db;
UserService(Database db) {
this.db = db;
}
void saveUser(String data) {
db.save(data);
}
}
Real-World Analogy
- SRP: A Swiss army knife with one function per tool
- OCP: Adding a new plugin without changing the core software
- LSP: Replacing a phone charger with another brand that fits
- ISP: Not forcing a coffee machine to print receipts
- DIP: Plugging any speaker into a headphone jack via AUX cable
Pros and Cons of SOLID
✅ Benefits
- Better maintainability
- Easier unit testing
- Cleaner, decoupled code
- Facilitates agile changes
❌ Challenges
- May overcomplicate simple scenarios
- Requires discipline and experience
- Can lead to class explosion if misapplied
Java 17/21 Notes
- Use sealed classes to control valid subclass hierarchies (helps with LSP and OCP)
- Use records to model immutable DTOs (fits SRP)
- Leverage modules and services for enforcing DIP
Best Practices
- Start applying SOLID during design, not after development
- Refactor regularly to align with principles
- Use interfaces wisely; avoid fat interfaces
- Combine with design patterns like Strategy, Adapter, and Factory
Conclusion
The SOLID principles are timeless design rules that help Java developers build clean, modular, and scalable systems. By applying them consistently, you future-proof your applications and improve collaboration across teams.
Even if you can’t apply all five at once, start with SRP and OCP—they deliver immediate benefits in most codebases.
Key Takeaways
- SRP: One reason to change per class
- OCP: Extend without modifying
- LSP: Subclasses must behave like their parents
- ISP: Avoid fat interfaces
- DIP: Depend on abstractions, not implementations
FAQs
1. Can I apply SOLID to small projects?
Yes. Start simple, especially with SRP and OCP.
2. Is SOLID only for OOP languages?
Primarily yes, but the ideas influence architecture in other paradigms too.
3. Does SRP apply to methods too?
Yes, though it's typically discussed at the class level.
4. Can I violate SOLID on purpose?
Yes, but only with clear justification—e.g., performance.
5. What’s the hardest SOLID principle to apply?
DIP and ISP can be tricky for beginners.
6. How does SOLID relate to design patterns?
SOLID principles form the basis of most patterns.
7. Do Java records violate SRP?
No. They're focused data carriers and align well with SRP.
8. Can SOLID reduce technical debt?
Absolutely. It promotes maintainable and testable code.
9. Should I force all code into SOLID from the start?
No. Apply iteratively and evolve with the project.
10. Do modern frameworks enforce SOLID?
Spring and others encourage it via DI, layered architecture, etc.