skip to content

What is the Dependency Inversion Principle (DIP), and how does a code-level rule about interfaces end up creating an architectural boundary?

level: middleimportance: must knowfreq 74%

answer

  1. Policy owns the abstraction, detail implements it
  2. Source arrow flips against the call arrow
  3. Ports & adapters = DIP at package scale
  4. Composition root is the only both-sides knower
  5. DI ≠ DIP; indirection ≠ inversion

basics

~20 s

DIP says high-level policy should not depend on low-level details — both should depend on an abstraction, and that abstraction should be owned by the policy side. In practice the business code declares the interface and the database or HTTP code implements it, so the dependency arrow points inward.

solid answer

~50 s

The Dependency Inversion Principle has two clauses: high-level modules must not depend on low-level modules — both depend on abstractions; and abstractions must not depend on details — details depend on abstractions. The crucial, often-missed part is **ownership**: the interface must belong to the consumer (the policy), not to the implementer. If the business layer declares `OrderRepository` and the SQL adapter implements it, then at compile time the arrow points from SQL → business, opposite to the runtime call direction business → SQL. That inversion is what turns a plain interface into a **boundary**: the detail becomes a plugin behind stable policy, it can be replaced or double-tested without touching policy, and the two sides can be compiled, released, and owned separately. Hexagonal/ports-and-adapters, Clean Architecture's dependency rule, and plugin architectures are all just DIP applied at package or deployable scale. The cost is indirection, so invert across volatility boundaries, not everywhere.

code

pseudocode · 20 lines
pseudocode
// package: domain (high-level policy) — OWNS the abstraction
interface OrderRepository {          // stated in domain terms only
    fun save(order: Order)
    fun dueBefore(date: Date): List<Order>
}

class CheckoutService(private val orders: OrderRepository) {
    fun checkout(cart: Cart) { orders.save(Order.from(cart)) }
}

// package: infrastructure (low-level detail) — DEPENDS ON domain
class SqlOrderRepository(private val db: Db) : OrderRepository {
    override fun save(order: Order) { db.exec("INSERT ...") }
    override fun dueBefore(date: Date) = db.query("SELECT ...").map { it.toOrder() }
}

// package: main (composition root) — the ONLY place that knows both
fun main() { CheckoutService(SqlOrderRepository(Db.connect(...))).run() }
// runtime calls:   domain --> infrastructure
// source imports:  infrastructure --> domain   <-- the inversion

go deeper

for a junior

State both clauses and give the concrete example: the service takes a repository interface in its constructor, the SQL class implements it, main wires them. Mention it makes testing without a database possible.

for a middle

Emphasize ownership of the abstraction and show that the compile-time arrow flips relative to the runtime call. Name the composition root. Distinguish DIP from DI/IoC.

for a senior

Scale it to boundaries: ports and adapters, the Clean Architecture dependency rule, driving vs. driven adapters, plugin/deployment independence. Discuss leaky ports, role vs. header interfaces, and where you deliberately don't invert.

for a principal

Frame it as volatility management and organizational decoupling: which axes of change you're buying optionality on, the cost of that optionality, how it maps to build/release units and team ownership, and how you audit compliance (dependency-direction tests, module systems, ArchUnit-style rules) rather than relying on convention.

## Statement of the principle Robert C. Martin's formulation (the "D" of SOLID): 1. **High-level modules should not depend on low-level modules. Both should depend on abstractions.** 2. **Abstractions should not depend on details. Details should depend on abstractions.** Definitions, since the words are slippery: - **High-level module / policy** = the code that expresses *what the system does* in business terms: pricing rules, order lifecycle, matching algorithm. It is the reason the system exists and it changes for business reasons. - **Low-level module / detail** = the mechanism: a specific database, message broker, file format, framework, UI toolkit, HTTP client. It changes for technology reasons. - **Abstraction** = an interface/protocol/abstract type stated purely in terms the policy cares about (`save(order)`, `findDueInvoices(date)`), with no mechanism-specific vocabulary (`ResultSet`, `HttpResponse`, `S3Object`). ## The inversion: it is about who *owns* the abstraction Most people stop at "program to an interface" and miss the actual inversion. Consider three arrangements where the business code calls the database at runtime: - **(a) No abstraction.** Business imports the SQL class directly. Source dependency: business → SQL. Changing the driver changes business code. - **(b) Interface owned by the detail.** The persistence package declares `SqlRepository` *and* the interface, business imports the interface from that package. Source dependency: still business → persistence package. Nothing was inverted; you only added a file. - **(c) Interface owned by the policy (true DIP).** The business package declares `OrderRepository` in its own terms. The persistence package imports the business package to implement it. Source dependency: persistence → business. **The compile-time arrow now points opposite to the runtime call arrow.** Only (c) is dependency inversion. Someone must still connect the two at startup: a `main`/composition root, a DI container, or a factory constructs the concrete adapter and hands it to the policy. That wiring code is the *only* place allowed to know both sides — this is why it belongs in the outermost layer. ## Why that single arrow flip is architectural A boundary is a line across which you can change one side without recompiling, retesting, or redeploying the other. Flipping the arrow gives you exactly that: - **Substitutability.** Swap Postgres for DynamoDB, or an in-memory fake in tests, without touching policy. - **Independent compilation/deployment.** The policy artifact has no reference to the detail artifact; the detail is a *plugin* loaded at runtime. - **Independent development.** Two teams can work against the agreed port. - **Volatility containment.** Volatile things (frameworks, vendors, wire formats) depend on stable things (business rules), never the reverse. This is the single most valuable structural rule in Clean Architecture's "dependency rule": source dependencies always point inward toward higher-level policy. - **Testability without infrastructure.** Policy tests need no container, network, or clock. Ports-and-adapters (Alistair Cockburn's *hexagonal architecture*), Clean/Onion architecture, and classic plugin systems are all the same move at larger granularity. **Driving** adapters (HTTP controller, CLI, scheduler) call inward and need no inversion — the arrow already points the right way. **Driven** adapters (database, mail, payment gateway) are called by policy, so they need DIP to keep the source arrow inward. ## Designing the port well The abstraction must be expressed in the language of the *consumer*, not the implementer. Failure modes: - **Leaky port.** `interface Repo { fun query(sql: String): ResultSet }` — the mechanism leaked through, so swapping storage still breaks policy. The interface is technically there, the inversion is not. - **Header interface vs. role interface.** Copying every public method of the existing concrete class into an interface (a "header interface") gives you a mirror of the detail. A **role interface** states only what the policy needs ("I need to look up prices"), which is also how DIP and the Interface Segregation Principle reinforce each other. - **Ownership placed wrong.** If the interface lives in the infrastructure package, you are in case (b). - **Types that leak.** The port's parameter and return types must also be policy-owned (domain types), or you have inverted the interface but not the data model. ## Related but distinct ideas - **Dependency Injection** is a *mechanism* for supplying a collaborator from outside (constructor parameters, setters, a container). You can inject a concrete class and gain nothing structurally. DIP is the *principle* about which side owns the abstraction. DI is common but not required — a `main` that builds the object graph by hand achieves DIP fine. - **Inversion of Control** is broader: the framework calls you rather than you calling it (template method, event loops, lifecycle callbacks). DIP is one specific application of that idea to source-code dependencies. - **Open–Closed Principle.** DIP is the usual implementation of OCP: new behavior arrives as a new implementation of the existing port, so policy is open for extension and closed for modification. ## Costs and when not to invert Every inversion buys optionality with indirection: an extra type, an extra hop when reading code, a runtime binding that static navigation can't always follow, and a composition root that grows. Symptoms of over-application are one-implementation interfaces everywhere, interface names that are just `IThing`, and stack traces you cannot follow. The discipline is to **invert across volatility boundaries** — where you have (or credibly expect) more than one implementation, or need to test policy without infrastructure, or need to protect a stable core from a churning vendor. For code that is stable and definitionally single (a date math helper, a value object), a direct dependency is honest and cheaper. A final subtlety: inverting the *source* dependency does not remove the *semantic* dependency. Policy still relies on someone actually persisting the order. DIP relocates and narrows the knowledge; it does not delete it.

  • How is DIP different from dependency injection?
    DI is a delivery mechanism — a collaborator is handed in from outside rather than constructed internally. DIP is a rule about the *direction and ownership* of source dependencies. You can inject a concrete infrastructure class (DI, no DIP) or achieve DIP with a hand-written composition root and no container at all.
  • Where should the interface live, and how would you tell in a code review that DIP has been faked?
    In the consumer's module. In review, check imports: if the domain package imports anything from the persistence/HTTP package — including the interface, its parameter types, or its exceptions — the arrow was never inverted. Also check whether the interface vocabulary is mechanism-flavored (`ResultSet`, `HttpResponse`), which is a leaky port.
  • Doesn't this end up with an interface per class and unreadable code?
    That's the failure mode of applying it uniformly. Invert where volatility or substitution exists — I/O, vendors, anything you must fake in tests. For stable, single-implementation code, a direct dependency is cheaper and more honest. One-implementation `IFoo`/`FooImpl` pairs across an entire codebase are a smell, not compliance.
  • Do driving adapters like an HTTP controller need DIP?
    Usually not. The controller already calls inward to policy, so the source arrow points the right way. DIP is needed for *driven* adapters — the things policy calls out to (database, mail, payment gateway), where the natural arrow points the wrong way.

A wall socket. The house wiring (policy) defines the socket shape; every appliance (detail) must conform to it. The house does not know about your kettle, the kettle knows about the socket standard. Swapping appliances requires no rewiring — and note the standard is owned by the building, not by the appliance vendor. If each vendor defined its own plug shape, you'd have case (b): an 'interface', no inversion.

saying these in an interview costs you the question

  • "DIP just means use interfaces" — without consumer ownership of the abstraction, the source dependency is unchanged and nothing was inverted.
  • Confusing DIP with dependency injection or with inversion of control generally.
  • Putting the port interface in the infrastructure package and calling the result hexagonal architecture.
  • Defining a port that leaks mechanism types (`ResultSet`, `HttpRequest`, ORM entities) — the interface exists but the coupling survives.
  • Mirroring a concrete class method-for-method (header interface) instead of stating the consumer's role.
  • Claiming DIP removes the dependency entirely; it relocates and narrows it, and something (the composition root) must still know both sides.
  • Applying it universally, producing `IFoo`/`FooImpl` for every class and untraceable stack traces.

context