Back

What is the dependency inversion principle? Explained simply

What is the dependency inversion principle? Explained simply

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

It’s a design principle where high-level modules and low-level modules both depend on abstractions instead of each other.

No. DIP is a principle. Dependency Injection is a technique to achieve DIP.

Because you can swap real dependencies for mocks or stubs that follow the same interface.

Interfaces help in TypeScript, but in JavaScript you can use object shape contracts and patterns to achieve the same.

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers