Skip to main content

Dependency Inversion Principle (DIP)

“High-level modules should not depend on low-level modules; both should depend on abstractions.” In short, depend on abstractions, not on concrete implementations.

Purpose

  • Decouples higher-level logic from low-level details, allowing those details to change without affecting high-level code.
  • Improves maintainability and testability – you can swap out or mock lower-level components (e.g., for unit tests or new requirements) without rewriting the core business logic.
  • Encourages layering and reuse: common abstractions can be defined and different implementations provided (for example, different database backends, different notification methods, etc.), all interchangeable from the perspective of the high-level code.

Minimal Example

In the first part below, ReportGenerator directly creates and uses a concrete EmailSender. This tight coupling means if we wanted to use a different notification method or change EmailSender, we’d have to modify ReportGenerator (a high-level module depending on a low-level one). The DIP-compliant solution introduces an abstract Notifier interface that ReportGenerator relies on. Concrete implementations like EmailNotifier and SMSNotifier can be injected into ReportGenerator. Now ReportGenerator depends only on the Notifier abstraction, not on any specific email/SMS class.

More Realistic Example

Consider an order processing system with multiple payment providers. Without DIP, the OrderService might directly instantiate a specific payment processor (say, Stripe), making it hard to switch to another provider. Using DIP, we define an abstract PaymentProcessor interface. OrderService depends on this abstraction and is provided an implementation (via constructor). This way, OrderService can work with any payment processor (Stripe, PayPal, etc.) interchangeably, and adding a new one doesn’t require altering OrderService’s code.