Start Learning
Javaneer
Back to stage
Stage 0·SOLID in Practice

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.

15 min readAdvanced
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 abstraction

The 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 wall socket, not a soldered cord

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.

Invert the dependency

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.
Was this lesson helpful?
Edit this page on GitHub