What is the dependency inversion principle? Explained simply
The Dependency Inversion Principle explains how abstractions decouple high-level and low-level modules in TypeScript, Python, and Java code.
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
What is the dependency inversion principle?
It’s a design principle where high-level modules and low-level modules both depend on abstractions instead of each other.
Is dependency inversion the same as dependency injection?
No. DIP is a principle. Dependency Injection is a technique to achieve DIP.
Why does DIP make testing easier?
Because you can swap real dependencies for mocks or stubs that follow the same interface.
Do I need interfaces to apply DIP in JavaScript?
Interfaces help in TypeScript, but in JavaScript you can use object shape contracts and patterns to achieve the same.