Module 02 · SOLID and friends · Lesson 5 of 7
Dependency inversion
Business rules should not depend on databases, frameworks or vendors; both should depend on abstractions the rules own. What the inversion is, ports and adapters, how it differs from injection, and where to stop.
Why it matters
When an order service calls a specific database driver, payment SDK and email library directly, its business rules are welded to those choices. Swapping a payment provider means editing checkout logic; testing a discount rule requires a database; upgrading a framework touches the core of the product. Dependency inversion arranges the code so the most valuable and stable part, the business rules, depends on nothing volatile, and the volatile details depend on it.
The idea
The principle has two parts. High-level modules should not depend on low-level modules; both should depend on abstractions. And abstractions should not depend on details; details should depend on abstractions. "High level" means policy (what the business does), "low level" means mechanism (how bytes get stored or sent).
The inversion is in the direction of the source-code dependency. Naturally, the order service calls the database code, so it imports and depends on it. Inverted, the order service declares the interface it needs (OrderRepository) in its own module, and the database code implements that interface, so the database module now depends on the core, not the other way round.
Ports and adapters
Applied to a whole service, dependency inversion gives the ports and adapters (hexagonal) architecture. The core contains the domain model and use cases plus ports: interfaces for everything it needs from the outside (storage, payment, notifications, time) and for how the outside drives it (use-case entry points). Adapters at the edge implement those ports for specific technologies (a Postgres adapter, a Stripe adapter, an HTTP controller, a message consumer). The core compiles and tests with no framework, database or network.
// core module: no imports from infrastructure
public interface PaymentGateway { ChargeId charge(Money amount, PaymentToken token, IdempotencyKey key); }
public final class PlaceOrder {
private final OrderRepository orders; private final PaymentGateway payments; private final Clock clock;
public PlaceOrder(OrderRepository orders, PaymentGateway payments, Clock clock) { ... }
public OrderId handle(PlaceOrderCommand cmd) { ... }
}
// infrastructure module: depends on core
public final class StripePaymentGateway implements PaymentGateway { ... }
Inversion, injection, containers
These are often conflated:
| Term | What it is |
|---|---|
| Dependency inversion (the principle) | Where interfaces live and which way source dependencies point: details depend on policy-owned abstractions |
| Dependency injection (a technique) | Passing a class its collaborators (usually through the constructor) instead of having it create them |
| Inversion of control | The framework calls your code rather than your code calling the framework; DI is one form |
| DI container (a tool) | A library that builds and wires object graphs automatically (Spring, Guice, .NET's built-in container) |
You can apply the principle with plain constructor injection and a hand-written composition root (one place at startup that builds the graph). A container is a convenience for large graphs, not a requirement, and injecting concrete classes through a container does not by itself invert anything.
Where to draw the line
Invert dependencies on things that are volatile or slow or external: databases, networks, vendors, frameworks, the clock, randomness, the file system. Do not invert dependencies on stable, in-process, side-effect-free code: the standard library, your own value objects, a pure calculation. An interface over String or Money adds nothing.
Trade-offs
Inverting dependencies makes the core testable without infrastructure, lets you replace vendors and frameworks at the edge, and keeps business rules readable. It costs interfaces, adapter classes, and a composition root to wire them, and over-applied it buries a small application under layers. Apply it at real seams; a short script or a CRUD admin tool can depend on its database directly.
In an interview
Draw the arrow direction explicitly: "The booking core defines SeatRepository, PaymentGateway and Clock; the Postgres, Stripe and system-clock adapters implement them. The core has no infrastructure imports, so all the booking rules are unit-tested with in-memory fakes, and switching payment provider is a new adapter." That is the principle, the payoff and the boundary in three sentences.
Check yourself
An OrderService imports a vendor's EmailClient class and calls it directly. Which change applies the dependency inversion principle rather than only dependency injection?