Skip to content

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 inversion flow

Dependency direction stays inward to abstractions. Concrete tools stay at the outer edge.

Dependency inversion bad versus good

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.

Dependency inversion change impact

Change impact map. Inversion keeps refactor cost near the adapter layer.

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.

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.