All Posts

Dependency Inversion Principle: Decoupling Your Code

High-level modules shouldn't depend on low-level modules. Both should depend on abstractions. We explain DIP with a simple Java example.

Abstract AlgorithmsAbstract Algorithms
ยทยท4 min read
Share
Share on X / Twitter
Share on LinkedIn
Copy link

TLDR: The Dependency Inversion Principle (DIP) states that high-level business logic should depend on abstractions (interfaces), not on concrete implementations (MySQL, SendGrid, etc.). This lets you swap a database or email provider without touching your business rules.


๐Ÿ“– The Tightly Coupled Trap: When Changing a Database Breaks Business Logic

You're building an order service. The first version looks like this:

class OrderService:
    def __init__(self):
        self.db = MySQLDatabase()          # direct dependency

    def place_order(self, order):
        self.db.save(order)
        EmailClient().send_confirmation(order)

Six months later, you migrate to PostgreSQL. You search for every place MySQLDatabase() is used โ€” it's woven through your order logic, your billing logic, your reporting. Changing the database means touching business code.

This is a dependency inversion violation: a high-level module (OrderService) depends directly on low-level modules (MySQLDatabase, EmailClient).


โš™๏ธ The Principle: Abstractions Own the Contract

The Dependency Inversion Principle has two parts:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
graph LR
    subgraph Before DIP
        Biz1[OrderService] --> MySQL[MySQLDatabase]
        Biz1 --> Email1[SendGridClient]
    end
    subgraph After DIP
        Biz2[OrderService] --> IRepo[IOrderRepository\nabstraction]
        Biz2 --> IEmail[IEmailService\nabstraction]
        IRepo --> MySQL2[MySQLRepository]
        IRepo --> PG[PostgresRepository]
        IEmail --> Email2[SendGridEmailService]
        IEmail --> Mock[MockEmailService]
    end

๐Ÿ”ข The Interface as the Contract: Python and Java Examples

Python (using ABC):

from abc import ABC, abstractmethod

class IOrderRepository(ABC):
    @abstractmethod
    def save(self, order) -> None: ...

    @abstractmethod
    def find_by_id(self, order_id: str): ...

class MySQLOrderRepository(IOrderRepository):
    def save(self, order):
        # MySQL-specific implementation
        ...

class PostgresOrderRepository(IOrderRepository):
    def save(self, order):
        # Postgres-specific implementation
        ...

class OrderService:
    def __init__(self, repo: IOrderRepository):   # depends on interface
        self.repo = repo

    def place_order(self, order):
        self.repo.save(order)

Switch to Postgres: OrderService(PostgresOrderRepository()). No changes to OrderService.

Java (using interface):

public interface IOrderRepository {
    void save(Order order);
    Order findById(String id);
}

public class OrderService {
    private final IOrderRepository repo;          // depends on interface

    public OrderService(IOrderRepository repo) {
        this.repo = repo;
    }

    public void placeOrder(Order order) {
        repo.save(order);
    }
}

๐Ÿง  Dependency Injection: How DIP Is Wired at Runtime

DIP says what to depend on (abstractions). Dependency Injection (DI) says how to supply the concrete implementation at runtime.

StyleExample
Constructor injectionOrderService(MySQLOrderRepository())
DI container (Spring)@Autowired IOrderRepository repo;
Factory functionmake_order_service(db="mysql")

Constructor injection is the simplest and most testable option for most cases.

Testing benefit: With DIP, unit tests can inject a mock:

class FakeOrderRepository(IOrderRepository):
    def __init__(self):
        self.saved = []
    def save(self, order):
        self.saved.append(order)

def test_place_order():
    repo = FakeOrderRepository()
    svc = OrderService(repo)
    svc.place_order(Order(id="1"))
    assert len(repo.saved) == 1

No database needed to test business logic.


โš–๏ธ When DIP Adds Complexity Without Value

DIP is not always the right answer:

SituationRecommendation
One concrete implementation, no plans to swapSkip the interface; add it later if needed
Utility/helper functions with no side effectsInterfaces add ceremony with no benefit
Scripts and one-off toolsDirect imports are fine
Library-internal codeOnly apply at public boundaries

A sign that DIP is applied incorrectly: your project has 40 interfaces, all of which have exactly one implementation. YAGNI โ€” don't abstract what you don't need.


๐Ÿ“Œ Key Takeaways

  • DIP: high-level modules + low-level modules should both depend on abstractions, not on each other.
  • The abstraction (interface) is the stable contract; the concrete implementation is the swappable detail.
  • The main win: you can swap databases, email providers, or payment gateways without touching business logic.
  • Dependency Injection is the mechanism that supplies the concrete implementation at runtime.
  • Don't over-apply DIP: only abstract at decision boundaries where swap is realistic.

๐Ÿงฉ Test Your Understanding

  1. PaymentService classes directly instantiate StripeClient. Which DIP rule does this violate?
  2. Why does DIP make unit testing easier?
  3. You have IEmailService with two implementations (SendGrid, SES). Is DIP being applied well? What would make it questionable?
  4. What is the difference between the Dependency Inversion Principle and Dependency Injection?

Abstract Algorithms

Written by

Abstract Algorithms

@abstractalgorithms