What is the dependency inversion principle? Explained simply

The Dependency Inversion Principle (DIP) is one of the five SOLID principles of object-oriented design. It helps create flexible, decoupled systems by shifting the direction of dependency — from concrete implementations to abstract contracts. This article will help you understand DIP in plain language, with examples you can apply right away.
Key Takeaways
- High-level modules should not depend on low-level modules; both should depend on abstractions
- DIP enables flexible and testable architecture
- You’ll see how to apply DIP with real-world examples in multiple languages
The official definition
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.
Let’s break this down:
- High-level module: contains business logic (e.g. placing an order)
- Low-level module: handles specific tasks (e.g. sending an email)
- Abstraction: an interface or base class that defines behavior, not implementation
A visual analogy
Imagine you have an OrderService
that sends an email notification when an order is placed.
Without DIP:
OrderService --> EmailService
OrderService
is tightly coupled to EmailService
. You can’t swap it or mock it easily.
With DIP:
OrderService --> INotificationService <-- EmailService
Now both modules depend on an abstraction (INotificationService
).
A code example: without DIP (TypeScript)
class EmailService {
send(message: string) {
console.log(`Sending email: ${message}`);
}
}
class OrderService {
constructor(private emailService: EmailService) {}
placeOrder() {
this.emailService.send("Order placed");
}
}
This tightly couples OrderService
to EmailService
.
Refactored: with DIP (TypeScript)
interface INotificationService {
send(message: string): void;
}
class EmailService implements INotificationService {
send(message: string) {
console.log(`Sending email: ${message}`);
}
}
class OrderService {
constructor(private notifier: INotificationService) {}
placeOrder() {
this.notifier.send("Order placed");
}
}
DIP in Python
from abc import ABC, abstractmethod
class NotificationService(ABC):
@abstractmethod
def send(self, message: str):
pass
class EmailService(NotificationService):
def send(self, message: str):
print(f"Sending email: {message}")
class OrderService:
def __init__(self, notifier: NotificationService):
self.notifier = notifier
def place_order(self):
self.notifier.send("Order placed")
# Usage
service = OrderService(EmailService())
service.place_order()
DIP in Java
interface NotificationService {
void send(String message);
}
class EmailService implements NotificationService {
public void send(String message) {
System.out.println("Sending email: " + message);
}
}
class OrderService {
private NotificationService notifier;
public OrderService(NotificationService notifier) {
this.notifier = notifier;
}
public void placeOrder() {
notifier.send("Order placed");
}
}
// Usage
OrderService service = new OrderService(new EmailService());
service.placeOrder();
Why DIP matters
- Testability: Swap real dependencies with mocks or fakes
- Flexibility: Switch implementations without touching high-level logic
- Separation of concerns: Each module does one job and communicates through contracts
Common misconception: DIP ≠ Dependency Injection
They’re related, but not the same:
- DIP is about who depends on whom (direction of dependency)
- Dependency Injection is one way to apply DIP — by injecting dependencies instead of hardcoding them
When to use DIP
Use it when:
- You want to write business logic that doesn’t care about the underlying implementation
- You’re working on a layered or modular application
- You’re building for testability or extensibility
Conclusion
The Dependency Inversion Principle is about flipping the usual direction of dependency — so that abstractions, not implementations, define your architecture. It makes your code more reusable, testable, and robust to change.
FAQs
It’s a design principle where high-level modules and low-level modules both depend on abstractions instead of each other.
No. DIP is a principle. Dependency Injection is a technique to achieve DIP.
Because you can swap real dependencies for mocks or stubs that follow the same interface.
Interfaces help in TypeScript, but in JavaScript you can use object shape contracts and patterns to achieve the same.