Dependency Inversion Principle

Topics Covered

What Is Dependency Inversion

The Coupling Problem in One Example

Why "Both Should Depend on Abstractions" Matters

DIP vs. Coding to an Interface

High-Level vs. Low-Level Modules

A Layered Architecture Without DIP

The Same Architecture With DIP

The Java Perspective

Why This Restructuring Matters at Scale

Abstractions and Dependency Injection

Constructor Injection

Setter Injection

DIP Enables Testing Through Mock Injection

The Composition Root Pattern

DIP in Practice

The Repository Pattern

Swapping Implementations at Runtime

The Java Repository Pattern

Common Anti-Patterns

Try It Yourself

Most developers learn to build software from the bottom up. You write a database class, then a service that calls the database, then an API that calls the service. Each layer reaches down and grabs the layer below it. This feels natural, but it creates a problem that gets worse as your codebase grows: every high-level decision depends on low-level implementation details.

The Dependency Inversion Principle (DIP) flips this relationship. It states two rules:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend on details. Details should depend on abstractions.

The word "inversion" refers to reversing the direction of dependency arrows. Instead of business logic pointing down toward infrastructure, both business logic and infrastructure point toward a shared abstraction in between.

Dependency arrows before and after DIP

The Coupling Problem in One Example

Consider a NotificationService that sends emails when a user signs up. The naive approach directly creates an SmtpEmailSender inside the service:

python
1class SmtpEmailSender:
2    def send(self, recipient: str, message: str) -> str:
3        # Connects to SMTP server, sends email
4        return f"Email sent to {recipient} via SMTP"
5
6class NotificationService:
7    def __init__(self):
8        self.sender = SmtpEmailSender()  # Hardcoded dependency
9
10    def send_notification(self, recipient: str, message: str) -> str:
11        return self.sender.send(recipient, message)

This looks simple, but the NotificationService now knows it uses SMTP. It knows the exact class name. It creates the dependency itself. These three facts create tight coupling that causes real pain:

  • Switching providers is surgery. Moving from SMTP to SendGrid means editing NotificationService, a class whose job is business logic, not email infrastructure.
  • Testing requires a real SMTP server. You cannot unit test NotificationService without either connecting to an actual mail server or adding complex mocking around a concrete class.
  • Reuse is impossible. If another team wants your notification logic but uses a different email provider, they cannot use your class without modifying it.

Why "Both Should Depend on Abstractions" Matters

DIP does not say "high-level modules should depend on abstractions." It says both should depend on abstractions. The low-level module (SMTP sender) also needs to conform to an interface rather than defining its own contract. This means the abstraction lives in the high-level module's territory: the business logic layer defines what it needs, and infrastructure adapts to match.

python
1from abc import ABC, abstractmethod
2
3class MessageSender(ABC):
4    @abstractmethod
5    def send(self, recipient: str, message: str) -> str:
6        pass

The MessageSender interface belongs to the business logic layer. It describes what the business needs: the ability to send a message to a recipient. It says nothing about SMTP, HTTP APIs, or any specific technology. The infrastructure layer then provides implementations that conform to this interface.

Interview Tip

DIP is not about using interfaces everywhere. It is about who owns the interface. When the high-level module defines the abstraction, it controls its own destiny. When the low-level module defines the interface, the high-level module is still coupled to infrastructure decisions.

DIP vs. Coding to an Interface

Some developers confuse DIP with the general advice to "code to an interface." There is an important distinction. You can code to an interface and still violate DIP if the interface is defined by the low-level module. For example, if the SMTP library ships its own ISmtpSender interface and your business logic depends on that, you are still coupled to SMTP: just through an interface instead of a concrete class. True DIP means the business logic layer defines the abstraction, and infrastructure implements it.