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.