Cruxly

Cruxly › Low-level design › Principles & Patterns

Dependency Inversion

depend on abstractions, not on concrete details

Open the interactive lesson

The principle: high-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions.

This is the principle that makes the other four buildable in practice — it's how you actually get a FeeStrategy into a CheckoutService without hardcoding which one.

Where it comes from, and where Martin took it. Martin published DIP in 1996, with exactly the two-part definition above. In Clean Architecture (2017) he scaled the same idea up from classes to a whole system as the Dependency Rule: "Source code dependencies must point only inward, toward higher-level policies." Business rules sit in the centre; the database, the framework and the UI sit outside and depend on the centre — never the other way round. So a class-level DIP fix and an architecture-level "keep the database out of the domain" decision are the same principle at two sizes.

The violation, concretely

class CheckoutService {
    private HourlyFee feeCalculator = new HourlyFee();   // concrete, hardcoded
    private MySqlTicketRepository repo = new MySqlTicketRepository();  // concrete, hardcoded

    void checkout(Ticket t) {
        Fee fee = feeCalculator.compute(t);
        repo.save(t);
    }
}

CheckoutService is meant to be policy — checkout orchestration — but it is welded to two concrete, low-level details: exactly one pricing scheme and exactly one database technology. Testing checkout() now requires a real MySQL connection. Swapping to weekend pricing means editing this class. Neither should be true of a class whose job is "orchestrate checkout," and both are true because the dependencies are constructed inside rather than handed in.

The fix — invert the dependency direction

class CheckoutService {
    private final FeeStrategy fees;          // abstraction
    private final TicketRepository repo;     // abstraction

    CheckoutService(FeeStrategy fees, TicketRepository repo) {  // handed in
        this.fees = fees;
        this.repo = repo;
    }

    void checkout(Ticket t) {
        Fee fee = fees.compute(t);
        repo.save(t);
    }
}

// wiring happens ONCE, at the edge of the program — not inside policy
CheckoutService service = new CheckoutService(new HourlyFee(), new MySqlTicketRepository());

CheckoutService now depends only on FeeStrategy and TicketRepository — interfaces it defines the shape of, that the concrete classes must conform to. This is the "inversion": traditionally CheckoutService (high-level) would depend on MySqlTicketRepository (low-level); now the low-level class depends on an abstraction the high-level class owns. Testing checkout() means passing an in-memory fake repository — no database required — and swapping databases means writing a new class, never touching CheckoutService.

This is where the rate limiter's clock injection comes from

The rate limiter exercise elsewhere in this track injects a LongSupplier clock rather than calling System.nanoTime() inline, for exactly this reason: RateLimiter is policy, and "what time is it" is a low-level detail RateLimiter should depend on abstractly, not concretely. The payoff named there — deterministic tests, no Thread.sleep — is the direct, practical benefit of Dependency Inversion, arrived at without ever naming the principle.

The tell to look for in your own code: a new SomeConcreteClass() inside a class whose job is describing policy or orchestration rather than infrastructure. If the class that contains that new would need to change every time the concrete class it constructs changes, the dependency points the wrong way.

What 'inversion' actually means — the arrow flips

The word inversion confuses people because nothing is upside down. What inverts is the direction of the source-code dependency relative to the direction of control flow.

BEFORE CheckoutService high level / policy depends on MySqlTicketRepository low level / detail Policy cannot be tested or reused without dragging MySQL along. AFTER CheckoutService high level / policy TicketRepository owned by the policy side implements MySqlTicketRepository low level / detail control still flows downward
Control still flows from CheckoutService down into MySQL at runtime. What changed is that the low-level class now points up at an interface the high-level module owns — so the detail depends on the policy, not the other way round.

Who owns the interface is the whole trick. If TicketRepository lives in the persistence package next to its MySQL implementation, you have gained almost nothing — the policy still points at the persistence module. Put the interface in the policy module, and the dependency genuinely reverses: the persistence package imports the policy package, never the reverse.

That is the difference between "I added an interface" and "I inverted a dependency," and it is the part that actually decides whether you can swap the database or ship the policy module without a database driver on the classpath.

Dependency Injection is the mechanism, not the principle

People use "Dependency Injection" and "Dependency Inversion" interchangeably and they are not the same thing. Inversion is the design principle (depend on abstractions). Injection is one mechanism for satisfying it (pass the concrete instance in via constructor, setter, or a framework).

You can follow DIP without a DI framework — the CheckoutService constructor above is manual, unframeworked dependency injection, and it is a complete and correct application of the principle. Frameworks (Spring, Guice) automate the wiring, which matters at scale, but the principle is satisfied the moment the dependency is handed in rather than constructed inside.

Constructor injection over setter injection, and it is worth having the reason ready: a constructor-injected dependency can be final, which means the object is never observable in a half-built state and its dependencies cannot be swapped underneath it at runtime. Setter injection permits an instance that exists but is not yet usable — a class of bug that only appears under specific initialisation orders, which is to say in production.

class CheckoutService {
    private final FeeStrategy fees;              // final: set once, at construction
    CheckoutService(FeeStrategy fees) { this.fees = fees; }
}
class CheckoutService {
    private FeeStrategy fees;                    // may be null right now
    void setFees(FeeStrategy fees) { this.fees = fees; }
}

Where the wiring lives, and when not to invert

If nothing constructs concrete classes, nothing runs. The concretes have to be chosen somewhere — the discipline is that "somewhere" is a single place at the outermost edge of the program, usually called the composition root.

public static void main(String[] args) {
    // the ONE place that knows every concrete type
    var repo    = new MySqlTicketRepository(dataSource);
    var fees    = new WeekendSurcharge(new HourlyFee());
    var service = new CheckoutService(fees, repo);
    startServer(service);
}

Everything inside is ignorant of which implementations it received. This also makes the whole dependency graph readable in one function — a genuinely useful property when joining a codebase, and something a DI framework's annotations scatter back across fifty files.

When not to invert. Not every dependency deserves an interface, and inverting everything produces a codebase where following any call path means three jumps through indirection that has exactly one implementation.

Depend on it directlyInvert it
String, List, Math — stable library typesAnything doing I/O: database, network, filesystem
Pure value objects and data classesAnything time- or randomness-dependent
Types within the same module changing togetherAnything you need to fake in a test
A concrete type with no plausible second implAnything a second implementation is expected for

The practical filter: invert what you need to replace in a test, or what crosses a boundary you might change. Everything else is ceremony.

:::warn The over-inversion smell An interface named IFooService with exactly one implementation named FooService, created solely so a mocking framework can stub it, is usually a sign the test should have been written against real collaborators instead. One implementation plus one mock is not two implementations. :::

How to say it in the interview

"The question I ask is: does the policy class construct its own dependency with new, or is the dependency handed to it? 'new' inside a high-level class is a concrete detail leaking upward into policy that should not know it exists."

Checklist

Practice this interactively
Share this lessonWhatsAppLinkedInX

Related lessons