Dependency Inversion
Depend on abstractions, not concretions - so policy doesn't hard-wire to details. The principle that makes code testable and sets up hexagonal architecture.
On this page
The Dependency Inversion Principle is the capstone of SOLID and the bridge to the rest of this path. It states two things: 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. In plain terms: your business logic should not be hard-wired to a specific database, HTTP client, or file format. Invert the dependency so the details plug into the policy, not the other way around.
The default direction, and why it hurts
Left alone, dependencies point downward: high-level policy calls low-level tools directly.
class FeeReportService { // high-level policy
private final PostgresTransactionDao dao = new PostgresTransactionDao(); // 🚩 concrete
private final SmtpEmailClient email = new SmtpEmailClient(); // 🚩 concrete
void sendMonthlyReport(String accountId) {
var txns = dao.query("SELECT ..."); // bound to Postgres
email.send(...); // bound to SMTP
}
}The important business logic now depends on Postgres and SMTP. You can't unit-test FeeReportService
without a real database and mail server; you can't swap Postgres for anything; a change to the DAO's constructor
reaches up into policy. The valuable, stable code is chained to the volatile, replaceable details.
Inverting the arrow
Introduce an abstraction owned by the policy, and make the detail implement it:
interface TransactionRepository { List<Transaction> findFor(String accountId); } // owned by the domain
interface ReportSender { void send(Report r); }
class FeeReportService {
private final TransactionRepository repo; // depends on the ABSTRACTION
private final ReportSender sender;
FeeReportService(TransactionRepository repo, ReportSender sender) { // injected
this.repo = repo; this.sender = sender;
}
void sendMonthlyReport(String accountId) {
var txns = repo.findFor(accountId);
sender.send(buildReport(txns));
}
}
class PostgresTransactionRepository implements TransactionRepository { ... } // detail depends on abstractionThe dependency arrow inverted: PostgresTransactionRepository now points up at the domain's interface,
instead of the domain pointing down at Postgres. High-level and low-level modules both depend on the
abstraction in the middle - and crucially, that abstraction is defined in terms of the domain's needs
(findFor(accountId)), not the database's API.
DIP vs. dependency injection
These are often confused. Dependency Inversion is the principle - point dependencies at abstractions.
Dependency Injection is one mechanism for supplying those abstractions (via constructor, as above). You
can honor DIP with injection, a factory, or a service locator; DI is just the most common tool. Spring's IoC
container is industrial-strength DI in service of DIP - but passing a TransactionRepository into a plain
constructor already inverts the dependency, no framework required.
Who owns the interface matters
The subtle, senior point: the abstraction should belong to the high-level module, not the low-level one. The domain defines TransactionRepository in the language of the domain; the persistence layer conforms to it. If you instead let the database module define the interface, you've only added a layer of indirection while policy still bends to details. 'Details depend on abstractions' means the detail adapts to the policy's contract - this is exactly the seam hexagonal architecture builds on next module.
A device soldered directly to the building's wiring (concrete dependency) can't be moved, tested on a bench, or swapped without an electrician cutting into the wall. Dependency inversion is the wall socket standard: the building and the appliance both conform to the socket shape (the abstraction). The lamp doesn't know or care what power plant is behind the socket, and the grid doesn't know what's plugged in - each depends only on the agreed interface. Note who defines the socket: the building code (the policy/standard), and every appliance maker conforms to it - details depending on the abstraction, not the reverse.
ledger-legacy's AuditLogger (core domain logic that must run in every transaction) directly instantiates and
calls a FileSystemWriter to append audit lines to a local file. The team now needs audit logs to go to a cloud
service in production but a fake in tests, and the direct dependency makes both impossible. Apply DIP: what
abstraction do you introduce, who owns it, and where do the concrete writers live?
What does the Dependency Inversion Principle say?
Key takeaways
- DIP: high-level and low-level modules both depend on abstractions; details depend on abstractions, not the other way around.
- The default dependency arrow points from policy down to concrete tools (a DB, an SMTP client), chaining valuable logic to volatile details.
- Invert it by depending on an interface and injecting the implementation, so details plug into policy and the domain becomes testable and swappable.
- Dependency Inversion is the principle; dependency injection (constructor, factory, Spring's container) is just a mechanism to achieve it.
- The abstraction should be owned by the high-level module and speak the domain's language - the seam that hexagonal architecture builds on next.