Last updated: Apr 29, 2026
Dependency Inversion
Your code should depend on abstractions, not concrete implementations. Instead of a service creating its dependency directly, it should receive an interface through the constructor.
This matters for both testing and flexibility. When a service is coupled to a concrete class, you can’t unit test without the real thing, and you can’t swap the implementation without modifying the service. When it depends on an interface, you inject a mock for testing or a different implementation for a different context.
Note that DIP is a design principle, while dependency injection — passing dependencies through the constructor — is just one technique for achieving it.
Example
NotificationService creates EmailSender directly, so it is tightly coupled to email:
class EmailSender {
send(message: string): void {
// send email
}
}
class NotificationService {
private sender = new EmailSender();
notify(message: string): void {
this.sender.send(message);
}
}
Define an interface based on what NotificationService needs, then inject the implementation:
interface MessageSender {
send(message: string): void;
}
class EmailSender implements MessageSender {
send(message: string): void {
// send email
}
}
class SmsSender implements MessageSender {
send(message: string): void {
// send SMS
}
}
class NotificationService {
constructor(private sender: MessageSender) {}
notify(message: string): void {
this.sender.send(message);
}
}
Now NotificationService knows nothing about email or SMS. You can swap the sender by passing a different implementation, and you can test with a mock that does not send real messages.