Introduction
The Dependency Inversion Principle (DIP) is the final letter of SOLID. It is frequently confused with Dependency Injection (DI), but they are different things: DIP is a design principle about which direction dependencies should point, while DI is a technique commonly used to satisfy that principle in code.
Cricket analogy: Deciding that a captain should always trust the bowling coach's plan rather than dictate tactics personally is the principle; actually handing the coach a tablet with live match data is the mechanism that makes it work.
Explanation
DIP has two parts: (1) high-level modules should not depend on low-level modules -- both should depend on abstractions, and (2) abstractions should not depend on details -- details should depend on abstractions. In a naive design, a high-level policy class (say, an OrderService) directly instantiates and calls a low-level implementation class (say, a MySqlOrderRepository). That creates a hard compile-time and runtime dependency from the important business logic down to a specific database technology, making the business logic hard to test and hard to swap later.
Cricket analogy: A captain who insists on personally texting only one specific specialist physio, rather than working through the team's medical staff protocol, can't function the day that physio is unavailable.
Dependency Injection is simply a mechanism for supplying a class's dependencies from the outside -- typically via its constructor, a setter, or a framework -- instead of the class constructing them itself. DIP tells you WHY to depend on an abstraction; DI is HOW you wire the concrete implementation into a class that only knows about that abstraction. You can use DI without DIP (injecting a concrete class directly), and you can achieve DIP without a DI framework (by manually passing an interface implementation into a constructor). They complement each other, but they answer different questions.
Cricket analogy: A team deciding it should always have a designated wicketkeeper is the principle; a coach handing the gloves to a specific new signing before the season starts is the mechanism that fulfills that need.
Example
# --- BEFORE: violates DIP ---
# OrderService (high-level) directly depends on a concrete, low-level class.
class MySqlOrderRepository:
def save(self, order: dict) -> None:
print(f"INSERT INTO orders ... {order}") # imagine real SQL here
class OrderService:
def __init__(self):
self.repository = MySqlOrderRepository() # hardcoded concrete dependency
def place_order(self, order: dict) -> None:
# business rules would go here
self.repository.save(order)
# Problem: OrderService can never be unit-tested without a real (or mocked-via-
# monkeypatch) MySqlOrderRepository, and switching databases means editing
# OrderService's source code.
# --- AFTER: DIP via constructor injection ---
from abc import ABC, abstractmethod
class OrderRepository(ABC):
"""Abstraction that both the high-level and low-level modules depend on."""
@abstractmethod
def save(self, order: dict) -> None:
...
class MySqlOrderRepository(OrderRepository):
def save(self, order: dict) -> None:
print(f"INSERT INTO orders ... {order}")
class InMemoryOrderRepository(OrderRepository):
"""Great for fast, isolated unit tests."""
def __init__(self):
self.saved = []
def save(self, order: dict) -> None:
self.saved.append(order)
class OrderService:
# Dependency Injection: the concrete repository is supplied via the
# constructor, so OrderService only ever knows about OrderRepository.
def __init__(self, repository: OrderRepository):
self.repository = repository
def place_order(self, order: dict) -> None:
self.repository.save(order)
# Production wiring:
# service = OrderService(MySqlOrderRepository())
# Test wiring -- no real database needed:
# fake_repo = InMemoryOrderRepository()
# service = OrderService(fake_repo)
# service.place_order({"id": 1, "total": 42})
# assert fake_repo.saved == [{"id": 1, "total": 42}]
Analysis
After the refactor, OrderService (high-level policy) and MySqlOrderRepository (low-level detail) both depend on the OrderRepository abstraction, satisfying DIP's first clause. The abstraction itself, OrderRepository, defines only a save() method meaningful to any storage technology, and does not leak SQL or MySQL-specific details, satisfying the second clause. Constructor injection is the mechanism -- the DI technique -- that lets production code wire in MySqlOrderRepository while tests wire in InMemoryOrderRepository, with zero changes to OrderService itself. This is why DIP and DI are often taught together: DIP defines the target dependency direction, and constructor injection is one of the most common, straightforward ways to achieve it.
Cricket analogy: After restructuring, both the captain's game plan and the specific fast bowler depend only on the agreed 'bowl to the plan' role, letting the coach swap in a different bowler for practice without changing the captain's strategy at all.
Key Takeaways
- DIP: high-level and low-level modules should both depend on abstractions, not on each other directly.
- DIP's second clause: abstractions should not depend on implementation details.
- Dependency Injection is a technique (constructor, setter, or framework-based) for supplying dependencies from outside a class.
- DI can be used without achieving DIP if the injected type is still a concrete class rather than an abstraction.
- Constructor injection against an interface is a common way to satisfy DIP while keeping classes easily testable.
Practice what you learned
1. What is the correct distinction between Dependency Inversion Principle and Dependency Injection?
2. In the refactored OrderService example, what does OrderService depend on?
3. Which statement about Dependency Injection is accurate?
4. Why does the BEFORE version of OrderService in the example violate DIP?
Was this page helpful?
You May Also Like
SOLID Principles Overview
A guided tour of the five SOLID design principles that make object-oriented code easier to maintain and extend.
Liskov Substitution and Interface Segregation
Understand why subtypes must honor their base type's contract and why fat interfaces should be split into focused ones.
Singleton and Factory Patterns
Learn how Singleton guarantees a single instance and how Factory Method delegates object creation to a central point.
Software Architecture Basics
An introduction to software architecture: the high-level structure of a system, its components, and how they interact.
Related Reading
Related Study Notes in Software Engineering
Browse all study notesMicroservices Study Notes
Software Architecture · 30 topics
Software EngineeringTesting & TDD Study Notes
Software Testing · 30 topics
Software EngineeringDesign Patterns Study Notes
Software Design · 30 topics
Software EngineeringGit & Version Control Study Notes
Bash · 40 topics
Software EngineeringSystem Design Study Notes
Architecture · 40 topics