Dependency Inversion Principle
Depend on abstractions, not concretions
Overview
The Dependency Inversion Principle (DIP) states that:
- 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.
This principle is the foundation of Dependency Injection and enables loose coupling.
Key Concepts
Depend on abstractions (interfaces), not concrete classes
High-level modules define interfaces
Low-level modules implement interfaces
Enables easy swapping of implementations
Foundation for Dependency Injection
Code Example
1// ❌ BAD: High-level depends on low-level (concrete classes)
2class MySQLDatabase {
3 public void save(String data) {
4 System.out.println("Saving to MySQL: " + data);
5 }
6}
7
8class UserService {
9 private MySQLDatabase database; // Direct dependency on concrete class!
10
11 public UserService() {
12 this.database = new MySQLDatabase(); // Creates its own dependency
13 }
14
15 public void createUser(String name) {
16 database.save(name);
17 }
18}
19
20// Problem: Can't switch to PostgreSQL without changing UserService!
21
22// ✅ GOOD: Both depend on abstraction
23interface Database {
24 void save(String data);
25 String find(String id);
26}
27
28class MySQLDatabase implements Database {
29 @Override
30 public void save(String data) {
31 System.out.println("MySQL: Saving " + data);
32 }
33
34 @Override
35 public String find(String id) {
36 return "MySQL data for " + id;
37 }
38}
39
40class PostgreSQLDatabase implements Database {
41 @Override
42 public void save(String data) {
43 System.out.println("PostgreSQL: Saving " + data);
44 }
45
46 @Override
47 public String find(String id) {
48 return "PostgreSQL data for " + id;
49 }
50}
51
52class UserService {
53 private final Database database; // Depends on abstraction!
54
55 // Dependency injected through constructor
56 public UserService(Database database) {
57 this.database = database;
58 }
59
60 public void createUser(String name) {
61 database.save(name);
62 }
63}
64
65// Usage - easy to swap implementations
66public class Main {
67 public static void main(String[] args) {
68 // Use MySQL
69 Database mysqlDb = new MySQLDatabase();
70 UserService service1 = new UserService(mysqlDb);
71
72 // Use PostgreSQL - same UserService code!
73 Database postgresDb = new PostgreSQLDatabase();
74 UserService service2 = new UserService(postgresDb);
75
76 // Use mock for testing
77 Database mockDb = new MockDatabase();
78 UserService testService = new UserService(mockDb);
79 }
80}UserService depends on Database interface, not specific implementations. Can easily switch databases or use mocks for testing.
When to Use
When you need flexibility in implementations
For testability (inject mocks)
When working with external services
In layered architectures
When building frameworks or libraries
Advantages
- ✓
Loose coupling between modules
- ✓
Easy to swap implementations
- ✓
Better testability
- ✓
More flexible and maintainable
- ✓
Supports plugin architectures
Best Practices
- 1.
Define interfaces at the boundary of modules
- 2.
Use constructor injection for required dependencies
- 3.
High-level module owns the interface definition
- 4.
Use DI frameworks (Spring, Guice) for complex apps
- 5.
Don't create dependencies inside classes
💡 Interview Tips
Explain both parts of the principle
Show database abstraction example
Connect to Dependency Injection
Discuss testing benefits (mock injection)
Mention DI frameworks (Spring, etc.)