Dependency inversion principle (DIP)
Open in the interactive app: practice, quiz, mock interview →In one minute
- High-level policy (the business rule) should not depend on low-level detail (the database driver, the email vendor, the system clock). Both should depend on an abstraction, and the policy side owns that abstraction.
- "Inversion" refers to the direction of the source-code dependency: control still flows from policy to database at runtime, but the import arrow points from the database adapter to the policy's interface.
- Dependency injection is the usual mechanism: pass the clock, repository and notifier in through the constructor instead of creating them inside. A composition root at the program's edge wires the real ones; tests wire fakes.
- Overdone, it becomes needless indirection: interfaces for things that never vary, DI containers that hide where objects come from, and stack traces ten frames deep.
- Agent-written code often calls
datetime.now(),os.environor an SDK client directly from business logic. It works, the happy-path test passes, and the code becomes impossible to test at the edges. Review for hidden dependencies.
How it works
The "before": policy welded to detail
A billing service sends reminders for unpaid invoices. The first version, written in a hurry:
import os, sendgrid, psycopg
from datetime import datetime, timedelta, timezone
class ReminderJob:
def run(self):
conn = psycopg.connect(os.environ["DB_URL"])
rows = conn.execute("SELECT id, email, due_at FROM invoices WHERE paid = false")
now = datetime.now(timezone.utc)
mail = sendgrid.SendGridAPIClient(os.environ["SG_KEY"])
for inv_id, email, due_at in rows:
if due_at - now < timedelta(days=3):
mail.send(build_message(email, inv_id))
conn.execute("UPDATE invoices SET reminded_at = now() WHERE id = %s", [inv_id])The rule that matters to the business is one line: remind when an invoice is due within three days. Everything else is detail, yet the rule cannot be tested without a Postgres database, a SendGrid key and control of the current time. Switching from SendGrid to SES means editing the class that holds the business rule.
The "after": policy owns its interfaces
Invert the dependencies. The reminder module declares what it needs, in its own terms:
class Clock(Protocol):
def now(self) -> int: ...
class InvoiceRepo(Protocol):
def unpaid(self) -> list[Invoice]: ...
def mark_reminded(self, invoice_id: str, at: int) -> None: ...
class Notifier(Protocol):
def send(self, customer: str, message: str) -> None: ...
class ReminderService:
def __init__(self, clock: Clock, repo: InvoiceRepo, notifier: Notifier):
self.clock, self.repo, self.notifier = clock, repo, notifierA separate adapters package contains PostgresInvoiceRepo, SesNotifier and SystemClock, each importing the reminder module to implement its interfaces. A main.py (the composition root) constructs them and passes them in. The reminder module imports nothing from Postgres or AWS. The import graph now points inward, toward the policy.
Tests now construct the service with a fixed clock, an in-memory repo and a recording notifier, and run in microseconds. Swapping vendors means writing a new adapter, not editing the rule. And the interfaces describe the domain (unpaid, mark_reminded) rather than the database (execute, cursor): the abstraction belongs to the policy, so it speaks the policy's language.
Inversion is not the same as injection
Dependency injection (passing collaborators in) is a technique. Dependency inversion is a statement about which module owns the abstraction. You can inject a psycopg.Connection into a service: that is injection without inversion, because the policy still depends on the driver's API. You can also invert without a framework: a constructor with three parameters and a 20-line main is enough for most services. DI containers (Spring, Guice, .NET's built-in container, Go's Wire or Fx, Python's dependency-injector) automate the wiring when the object graph gets large, at the cost of making it less visible.
When inversion is overkill
Not every dependency deserves an interface. The standard library's string functions, a pure math helper, an internal data class: these are stable, deterministic and fast, and wrapping them adds nothing. The useful test is volatility and side effects. Invert a dependency when it does I/O, reads time or randomness, has a vendor you might replace, or is slow enough to keep out of unit tests. Leave it concrete otherwise.
Signs of over-application: an IStringFormatter; a UserServiceImpl that is the only implementation of UserService and has no fake; a container configuration file longer than the code it wires; new engineers unable to find which class runs in production without starting the app.
The same shape elsewhere
This is the trust-but-verify shape. You can only verify what you can control, and you can only control what you inject. The same idea appears in hardware (a device driver implements the kernel's interface, not the reverse), in plugin systems (a browser defines the extension API and extensions depend on it) and in AI tooling: an agent framework that defines a Tool interface and lets you plug in implementations is dependency inversion, and it is what makes agent runs replayable in tests with fake tools.
Down the stack
Layer 1The contract
The policy module promises behaviour in terms of its own interfaces: given a clock reading, a repository's contents and a notifier's responses, it produces a defined sequence of calls. Testable form: with injected fakes, the same inputs always give the same calls and the same result, and the module's import list contains no infrastructure package.
Layer 2The mechanism
Constructor injection stores references in fields; each call goes through an interface. Wiring happens once, at startup, in a composition root. Containers automate that step in different ways. Spring scans the classpath, reads annotations by reflection and builds a bean graph, often creating proxies for scoped or transactional beans. Go's Wire generates plain constructor calls at compile time, so there is no runtime container. Uber's Fx resolves the graph by reflection at startup. In Python and TypeScript, most teams pass dependencies explicitly; monkeypatching is a weaker substitute, because it patches a module global rather than the one object under test.
Package boundaries enforce the inversion. Go forbids import cycles, so if adapters imports reminder, reminder cannot import adapters; the compiler checks the direction. Tools such as import-linter for Python and ArchUnit for Java let you write that rule for languages that do not enforce it.
Layer 3The machine
Each call through an injected interface is an indirect call: about 1 to 3 ns with a warm branch predictor, plus a lost inlining opportunity. Compared to what these dependencies do (a Postgres query at 0.5 to 5 ms, an HTTP call to an email vendor at 50 to 300 ms) that is under one part in 100,000. Injection is effectively free on I/O paths. It is not free in a tight CPU loop, where an injected Hasher called 100 million times can cost a few hundred milliseconds that a direct call would not; keep inversion at I/O boundaries, not inside hot kernels.
The visible runtime costs come from containers. Reflection-based wiring at startup can take hundreds of milliseconds to several seconds in large Spring applications, which matters for serverless cold starts; ahead-of-time processing (Spring AOT, GraalVM native images) or compile-time wiring (Wire, Dagger) moves that work to build time. Proxies add a frame and sometimes an allocation per call. Injected singletons are shared across threads or goroutines, so a fake that is safe in a single-threaded test may race in production; real adapters need their own locking or connection pools.
Why it's built this way
- Business rules change at one rate, infrastructure at another; coupling them makes each change pay for both.
- Side effects (time, network, disk) make code slow and nondeterministic to test.
- So isolate side effects behind interfaces the policy owns, and pass implementations in.
- Interfaces add an indirection per call, which costs nanoseconds against milliseconds of I/O.
- Wiring large graphs by hand gets tedious, so containers emerged, and their own costs (startup, opacity) are why many teams still prefer a hand-written composition root.
Diagram
flowchart LR Main["main: composition root"] --> Svc["ReminderService"] Main --> PG["PostgresInvoiceRepo"] Main --> SES["SesNotifier"] Svc --> IRepo["InvoiceRepo interface"] Svc --> INot["Notifier interface"] PG -.->|implements| IRepo SES -.->|implements| INot
Worked example
The reminder job runs hourly over about 40,000 unpaid invoices. Before the refactor, its test suite had two tests, both needing Docker Postgres; the suite took 38 seconds and failed about once a week when the run crossed midnight UTC, because datetime.now() moved during the test. No test covered the "notifier failed for one customer" case, and in production a single SendGrid 500 aborted the loop, so every invoice after the failing one was skipped until the next hour.
After inversion, the team wrote 14 unit tests with a fixed clock, an in-memory repo and a notifier fake that can be told to fail for specific customers. The suite runs in 0.2 seconds. The failure test immediately exposed the abort bug: the fix was to catch the send error, leave that invoice unmarked so it is retried next hour, and continue. They also found that the "remind at most once per 24 hours" rule had an off-by-one at exactly 86,400 seconds, which was invisible with a real clock.
One integration test still runs the Postgres adapter against a real database to check the SQL. That is the right split: policy tested with fakes, adapter tested against the real thing.
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| Direct instantiation inside policy | Simple, easy to navigate | Untestable side effects, vendor lock-in in the rule | Scripts and prototypes |
| Constructor injection, hand-written root | Explicit, no magic, fast startup | Wiring boilerplate in large graphs | Most services |
| Reflection DI container | Wires large graphs, scopes, lifecycles | Startup cost, hidden wiring, runtime errors | Large Java or .NET apps with established conventions |
| Compile-time DI (Wire, Dagger) | Checked wiring, no runtime cost | Code generation step | Large Go or Android graphs |
| Monkeypatching globals in tests | No production changes | Brittle, leaks between tests, hides design problems | Legacy code before a refactor |
Pitfalls in production
- Interfaces shaped like the vendor SDK (
send_email(template_id, dynamic_template_data)) instead of the domain. Fix: name methods in the policy's terms; the adapter translates. - Fakes that drift from real behaviour, so tests pass and production fails. Fix: run the same contract tests against the fake and the real adapter.
- Hidden dependencies that slipped back in:
time.Now(),uuid4(),os.Getenvinside the service. Fix: grep for them in review and lint for them in policy packages. - A container that silently injects the wrong implementation when two match. Fix: fail startup on ambiguity and log the resolved graph.
- Injected singletons with mutable state shared across requests. Fix: make adapters stateless or guard them, and test them under concurrency.
- Interfaces for every class, including value objects. Fix: invert only at I/O, time, randomness and replaceable vendor boundaries.
Leadership lens
- Decide where the boundaries are as a team: which packages are policy, which are adapters, and which direction imports may go. Then enforce it with a linter, not code review memory.
- When reviewing agent-written changes, check the import list of policy modules first. Agents reach for the nearest working call, often
datetime.now()or a concrete SDK client, and the tests they write alongside tend to mock at the wrong level. - Prefer a plain composition root until the wiring is genuinely painful. A container is a team-wide dependency with its own learning curve and failure modes.
- Watch the cost of fakes: each is code to maintain. Shared, contract-tested fakes for the main adapters pay off; ad hoc mocks in every test file do not.
Check yourself
What exactly is inverted in dependency inversion?
Show answer
The direction of the source-code dependency. The low-level adapter imports and implements an interface owned by the high-level policy, even though at runtime the policy calls the adapter.
A service takes a
psycopg.Connectionin its constructor. Is that dependency inversion?Show answer
It is injection but not inversion: the policy still depends on the driver's API. Inversion needs an interface in the policy's own terms, such as
InvoiceRepo.unpaid().Why is injecting an interface essentially free for a repository call but possibly costly for a hash function?
Show answer
The indirect call costs nanoseconds, which vanishes against milliseconds of database I/O but adds up when called hundreds of millions of times in a CPU-bound loop where inlining matters.
Why should time be an injected dependency?
Show answer
Real time makes tests nondeterministic and makes boundary cases (exactly 24 hours later, midnight, DST) impossible to reproduce; an injected clock lets tests set it precisely.
Watch
- Dependency Injection, The Best Pattern (CodeAesthetic): a short, concrete refactor from hard-wired dependencies to injected ones.
- The Clean Code Talks: Don't Look For Things! (Google TechTalks): Misko Hevery on asking for dependencies instead of fetching them.
- The Clean Code Talks: Unit Testing (Google TechTalks): why testability depends on how objects get their collaborators.
Free resources
- Dependency inversion principle (Wikipedia): the two rules, ownership of abstractions and common variations.
- DIP in the Wild (Brett Schuchert, martinfowler.com): worked examples of inversion in real code, including where it goes wrong.
- Inversion of Control Containers and the Dependency Injection pattern (Martin Fowler): the article that named dependency injection and compared its forms.
- Mocks Aren't Stubs (Martin Fowler): the kinds of test doubles and when each fits.
- Clean Code Talks: Dependency Injection (Google Testing Blog): a short write-up of injection for testability.