Introduction
As Java applications grow in size and complexity, maintainability and modularity become critical. Two core SOLID principles help address these challenges:
- Dependency Inversion Principle (DIP)
- Interface Segregation Principle (ISP)
Together, they encourage clean boundaries between components, greater testability, and highly extensible architecture.
In this guide, we’ll break down both principles, show real-world examples, identify common misuses, and teach you how to apply them in your Java projects.
What Is the Dependency Inversion Principle (DIP)?
Definition
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Key Concepts
- High-level module = business logic
- Low-level module = utility, storage, etc.
- Abstraction = interface or abstract class
🚫 DIP Violation Example
class MySQLDatabase {
void save(String data) {
System.out.println("Saving to MySQL: " + data);
}
}
class UserService {
private MySQLDatabase db = new MySQLDatabase();
void register(String user) {
db.save(user);
}
}
UserService
is tightly coupled toMySQLDatabase
.
✅ DIP Compliant Example
interface Database {
void save(String data);
}
class MySQLDatabase implements Database {
public void save(String data) {
System.out.println("Saving to MySQL: " + data);
}
}
class UserService {
private final Database db;
UserService(Database db) {
this.db = db;
}
void register(String user) {
db.save(user);
}
}
UserService
now depends on the interface, not the concrete class.
What Is the Interface Segregation Principle (ISP)?
Definition
Clients should not be forced to depend on interfaces they do not use.
Key Concepts
- Avoid "fat" interfaces.
- Split interfaces by behavior or responsibility.
🚫 ISP Violation Example
interface Worker {
void work();
void eat();
}
class Robot implements Worker {
public void work() {
System.out.println("Robot working");
}
public void eat() {
throw new UnsupportedOperationException("Robots don't eat!");
}
}
✅ ISP Compliant 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"); }
}
UML Diagram Overview
<<interface>> Database
↑
----------------
| |
MySQLDatabase MongoDatabase
<<class>> UserService
- db: Database
<<interface>> Workable <<interface>> Eatable
↑ ↑
Human Human
↑
Robot
Real-World Use Case: Spring Framework
In Spring, DIP is implemented using dependency injection:
@Service
public class OrderService {
private final PaymentProcessor processor;
@Autowired
public OrderService(PaymentProcessor processor) {
this.processor = processor;
}
public void checkout() {
processor.process();
}
}
- Spring injects dependencies via constructor injection.
Pros and Cons
✅ Advantages
- Modular codebase
- Swappable implementations
- Easy unit testing with mocks
- Aligned with SOLID and clean architecture
❌ Drawbacks
- More interfaces = more files
- May be overkill for small or static systems
- Can be misapplied without discipline
Common Misuse Cases
- Forcing all behaviors into a single interface
- Tightly coupling services to low-level details
- Using interfaces without real abstraction benefit
- Not injecting dependencies properly (new-ing objects)
Java 17/21 Enhancements
- Sealed Interfaces: Limit who can implement an interface, improving safety and predictability.
- Records: Lightweight immutable data holders that work well with ISP by encapsulating pure data behavior.
Real-World Analogy
- DIP: Think of a laptop charger that uses a USB-C port. The laptop doesn’t care about the power source as long as it conforms to the USB-C spec.
- ISP: Imagine you’re ordering food via a food delivery app. You don’t want to be forced to interact with restaurant kitchen management features.
Refactoring Example
Before: Violates DIP and ISP
interface UserOperations {
void create();
void delete();
void backupToFile();
}
class FileUserManager implements UserOperations {
public void create() {}
public void delete() {}
public void backupToFile() {}
}
✅ After: Compliant with DIP and ISP
interface UserManagement {
void create();
void delete();
}
interface UserBackup {
void backupToFile();
}
class FileUserManager implements UserManagement, UserBackup {
public void create() {}
public void delete() {}
public void backupToFile() {}
}
Best Practices
- Always code to interfaces, not implementations.
- Split large interfaces based on responsibility.
- Use constructor injection to enforce DIP.
- Apply composition over inheritance for better flexibility.
- Keep interfaces small and purpose-driven.
Conclusion
The Dependency Inversion and Interface Segregation principles enable clean, decoupled, and testable object-oriented design in Java.
By depending on abstractions and splitting responsibilities into focused interfaces, you lay the foundation for maintainable, scalable, and collaborative software.
Key Takeaways
- DIP: Depend on interfaces, not concrete classes
- ISP: Create small, focused interfaces
- Use composition + constructor injection
- Avoid fat interfaces and tightly coupled modules
- Java and Spring naturally support these principles
FAQs
1. Can I use DIP without Spring?
Yes. Constructor-based injection works without any framework.
2. Does DIP mean never using concrete classes?
No, it means depending on abstractions where variability is expected.
3. Is ISP about microservices?
Not directly. It applies to interface design inside any system.
4. Should I apply ISP from the start?
Yes—start small and evolve as responsibilities grow.
5. Can enums implement interfaces for DIP?
Yes. Enums are a great way to model simple strategies or commands.
6. How do sealed interfaces help?
They prevent accidental implementations and enforce tight control.
7. Is ISP violated by extending multiple interfaces?
Not necessarily. Violations occur when classes are forced to implement unused methods.
8. Is DIP just another name for dependency injection?
Not quite—DI is a mechanism to achieve DIP.
9. Should every class implement an interface?
Not always. Use interfaces when abstraction or testability is needed.
10. Does Java enforce these principles?
No. These are design principles, not enforced by the language.