Skip to main content
Articles/architecture/dependency-inversion-principle
Series: Low-Level Design Interview Prepβ€’Part 5 of 17
Scenario Article

Dependency Inversion Principle: Decoupling Your Code

The Tightly Coupled Trap: When Changing a Database Breaks Business Logic

Abstract Algorithmsβ€’Mar 9, 2026
XLinkedInReddit
πŸ’‘ Executive Summary

You're building an order service.

🎯 System Scenario & Requirements

System Goal

You're building an order service.

Actors & Components

  • Application Workers
  • Volatile Memory Tier
  • Persistent Disk Tier
  • Worker Process
  • In-Memory Cache
  • Primary Database

Functional Requirements

  • You're building an order service.
  • The first version looks like this: 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.

Constraints & Assumptions

  • DIP is not always the right answer: 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.

⚑ Execution Stages & Decision Trace

1

Stage 1 β€” The Two Rules of Dependency Inversion

Action / Decision:

The Dependency Inversion Principle has exactly two rules: 1.

Component Behavior:

High-level modules should not depend on low-level modules.

Observable Output: Both should depend on abstractions.
Read Full Stage in Deep Dive ↓
πŸ“–

Deep Dive

Detailed Architectural Analysis

Scenario context

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

Observable outcome: Abstractions should not depend on details.

Stage 3 β€” DIP Dependency Direction

  • Without DIP, high-level modules point arrows at low-level modules.
  • With DIP, both point at abstractions in the middle β€” the direction of dependency is inverted.
  • Reading the diagram: In the "With DIP" half, every arrow points AT the interface (the abstraction), never away from it.
  • OrderService knows nothing about MySQL or SendGrid β€” only about the interface contracts.
flowchart LR
    subgraph Without_DIP[Without DIP]
        A[OrderService] --> B[MySQLDatabase]
        A --> C[SendGridClient]
    end
    subgraph With_DIP[With DIP]
        D[OrderService] --> E[IOrderRepository]
        D --> F[IEmailService]
        G[MySQLRepository] --> E
        H[PostgresRepository] --> E
        I[SendGridEmailService] --> F
        J[MockEmailService] --> F
    end

Observable outcome: OrderService knows nothing about MySQL or SendGrid β€” only about the interface contracts.

Stage 4 β€” The Interface as the Contract: Python and Java Examples

  • Python (using ABC): Switch to Postgres: OrderService(PostgresOrderRepository()) .
  • Java (using interface): πŸ“Š DIP Correct Pattern: Both Layers Depend on the Abstraction This diagram shows the corrected DIP pattern: OrderService now points to the abstractions IOrderRepository and IEmailService rather t…
  • Both MySQLOrderRepository and PostgresOrderRepository implement IOrderRepository from below, so swapping database engines requires zero changes to OrderService .
  • MockEmailService implementing IEmailService is precisely what enables unit testing β€” tests inject the mock and verify behavior without sending real emails or touching a real database.
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)
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);
    }
}

Observable outcome: MockEmailService implementing IEmailService is precisely what enables unit testing β€” tests inject the mock and verify behavior without sending real emails or touching a real database.

Stage 5 β€” Deep Dive: Dependency Injection at Runtime

  • DIP says what to depend on (abstractions).
  • Dependency Injection (DI) says how to supply the concrete implementation at runtime.
  • Constructor injection is the simplest and most testable option for most cases.
  • Testing benefit: With DIP, unit tests can inject a mock: No database needed to test business logic.
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

Observable outcome: Testing benefit: With DIP, unit tests can inject a mock: No database needed to test business logic.

Stage 6 β€” Real-World Application: DIP Across Systems

  • Spring Boot example: Spring's @Autowired and @Repository / @Service annotations are DIP at the framework level.
  • You declare @Autowired IUserRepository repo in a service, and Spring injects the appropriate implementation.
  • Business logic never instantiates infrastructure classes directly.
  • Hexagonal architecture (Ports and Adapters): This architecture pattern IS DIP at the system design level.

Observable outcome: Hexagonal architecture (Ports and Adapters): This architecture pattern IS DIP at the system design level.

End-to-end outcome

After working through the stages, you should be able to explain:

    • DIP makes testing a first-class concern.
  • When business logic depends on interfaces, every external dependency (DB, email, HTTP) can be swapped for a fake in tests.

Alternatives considered

Decision Guide

Decision Guide

Failure and degraded modes

Trade-offs & Failure Modes: When DIP Adds Complexity

  • Detection: DIP is not always the right answer: A sign that DIP is applied incorrectly: your project has 40 interfaces, all of which have exactly one implementation.
  • User or operator impact: DIP is not always the right answer: A sign that DIP is applied incorrectly: your project has 40 interfaces, all of which have exactly one implementation.

Share this case study

Discuss architecture trade-offs and lessons with your team.

Share:XLinkedInReddit