Dependency Inversion
Core Idea
Dependency inversion keeps core rules independent from infrastructure tools. High-level policy depends on abstractions. Low-level details implement those abstractions. Dependency direction points toward stable contracts.
Conceptual Overview
This principle protects domain code from technical churn. A domain class should not import storage drivers, HTTP clients, or system clocks. It should depend on interfaces that define behavior.
Benefits:
- Business rules stay focused on domain decisions
- Infrastructure can change with lower blast radius
- Tests run fast with fakes and stubs
A quick check helps in reviews. If domain code imports concrete infrastructure packages, the boundary is leaking.
All examples below use the shared handbook application context: Hypothetical Scenario.
Dependency direction stays inward to abstractions. Concrete tools stay at the outer edge.
Bad and good dependency direction side by side. Policy stays stable in the good model.
Computing History
In 1996, Robert C. Martin formalized the Dependency Inversion Principle. The principle entered mainstream practice through SOLID guidance. Teams used it to keep policy code stable as frameworks and databases changed.
Sources: Martin (2003)
Quote
"High-level modules should not depend on low-level modules. Both should depend on abstractions."
Source: Robert C. Martin, 2003
Generic Example: Time Contract
from __future__ import annotations
from abc import ABC, abstractmethod
from datetime import datetime, timedelta, timezone
class TimeProvider(ABC):
@abstractmethod
def now(self) -> datetime:
raise NotImplementedError
class MatchRefreshSchedule:
def __init__(self, time_provider: TimeProvider):
self.time_provider = time_provider
def next_refresh_at(self) -> datetime:
return self.time_provider.now() + timedelta(hours=24)
The domain class depends on TimeProvider.
It does not call datetime.now() directly.
Anti Pattern and Better Version
Non compliant:
from datetime import datetime, timedelta, timezone
class MatchRefreshSchedule:
def next_refresh_at(self) -> datetime:
return datetime.now(timezone.utc) + timedelta(hours=24)
Issue:
- Policy code depends on a concrete system API
- Testing time-dependent behavior becomes fragile
Compliant:
class SystemTimeProvider(TimeProvider):
def now(self) -> datetime:
return datetime.now(timezone.utc)
MatchRefreshSchedule stays unchanged.
Only wiring selects which provider to use.
Change impact map. Inversion keeps refactor cost near the adapter layer.
Foundational Principle Links
This principle links to Abstraction and Boundaries. Abstractions define the contract at each boundary.
This principle links to Modularity and Composition. Dependency direction protects module cohesion during composition.
This principle links to Simplicity First. Policy code stays small when infrastructure detail stays outside.
This principle links to Correctness and Testing. Stable abstractions improve deterministic test design.
Architecture Link
This principle links to Onion Architecture. Inner layers own policy and depend on contracts.
This principle links to Hexagonal Architecture. Ports represent abstractions. Adapters hold external details.
Practice Checklist
- Keep domain imports free of infrastructure packages
- Define interfaces near policy code
- Place concrete implementations in outer layers
- Point compile-time dependencies toward abstractions
- Test policy classes with deterministic fakes
Written by: Pedro Guzmán
See References for complete APA-style bibliographic entries used on this page.