open atlas
↑ Back to track
Architecture Patterns ARCH · 02 · 02

Dependency inversion

DIP: high-level modules should not depend on low-level modules — both depend on abstractions. The key insight: the consumer, not the provider, owns the interface. This is the mechanism behind hexagonal and clean architecture boundaries.

ARCH Middle ◷ 22 min
Level
FoundationsJuniorMiddleSenior

The billing team at the B2B order platform designed a clean interface for the order repository. They put it in the order module’s package: order/interfaces/IOrderRepository.ts. Billing imported it from there. The arrangement looked tidy — the interface was right next to the implementation. But six months later the order team decided to restructure their package. They moved the interface, split it, and changed some method signatures. Billing broke. The payment gateway broke. The audit service broke. The order team was confused: they had published an interface, so they thought consumers were protected. They were not, because the interface lived in the order module’s package. The order module still owned it. Every time the order team touched their package, they could break every consumer — interface or no interface. The team had the right idea (an interface as the boundary) but the wrong ownership model. The Dependency Inversion Principle is not just about having an interface. It is about who owns it.

The two halves of DIP

Robert Martin’s Dependency Inversion Principle has two statements, and both must hold:

  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.

The first statement is familiar — it is the dependency rule from the previous lesson in module form. High-level modules (billing, in the B2B platform) contain the policy: the business logic that decides what should happen. Low-level modules (the order repository, the payment gateway) contain the mechanism: the technical plumbing that makes it happen. Policy should not be coupled to mechanism.

The second statement is the subtle one — and it is what the opening story got wrong. Putting the interface in the low-level module’s package makes the abstraction a detail of the low-level module. The high-level consumer imports the interface from the low-level module. The low-level module can still break the consumer by restructuring its package, renaming its interface, or splitting it into multiple interfaces. The abstraction is technically present, but ownership has not been inverted.

The DIP requires that the high-level module own the abstraction it depends on.

Who owns the interface?

Interface ownership is the core of DIP. An interface is owned by whoever defines it and controls its evolution. When you ask “should this interface change?”, the answer should come from the consumer’s needs, not from the provider’s implementation convenience.

In the B2B platform, the billing module needs order data. The interface it needs — IOrderProvider or IOrderPort — should live in billing’s package (or in a shared boundary package that billing controls). It declares exactly what billing requires: getConfirmedOrder(id: string): Promise<ConfirmedOrderDTO>. Nothing more. The order module then implements this interface by writing a class that satisfies billing’s contract.

The dependency arrows now run:

BillingService   ──imports──→   IOrderPort   (owned by billing)

                              implements

                         PostgresOrderRepository   (in order module)

The order module’s repository depends on billing’s interface — not the other way around. If billing’s needs change, the interface changes and the repository must update. If the order team changes how they fetch order data internally, they update their repository implementation; billing’s interface is untouched.

The compile-time boundary vs the runtime boundary

DIP is fundamentally about drawing a boundary that the compiler can enforce. On one side of the boundary: the high-level module and the interface it owns. On the other side: the low-level module that implements it. Neither side needs to know about the other’s concrete details.

This distinction between what can be known at compile time and what is resolved at runtime is the key to modularity:

At compile time, BillingService only knows about IOrderPort. It knows the method signatures, the parameter types, the return types. It knows the contract. It does not know that PostgresOrderRepository exists. The compiler cannot even see PostgresOrderRepository from billing’s perspective.

At runtime, something must wire IOrderPort to PostgresOrderRepository. This is the job of dependency injection: a composition root or a container resolves which concrete implementation satisfies which interface, and passes it to billing. At that moment, control flows from billing through the interface to the repository implementation.

This separation is the core of what makes architectural patterns like hexagonal architecture (unit 04) and clean architecture (unit 05) work. The hexagon in hexagonal architecture is the high-level domain, surrounded by interface definitions it owns (ports). Adapters — the concrete implementations — live outside the hexagon and depend on the ports inward. The concentric circles in clean architecture are layers where each inner circle owns the interfaces that outer circles implement. DIP is the mechanism that makes those patterns structurally enforceable.

Why this works

Why does interface ownership matter so much in practice? Because without it, the abstraction is a fiction. The interface may exist as a TypeScript interface or a Java abstract class, but if it lives in the low-level module’s package, the low-level team can change it. They can add required methods, change signatures, deprecate and remove things — and the high-level consumer breaks. The word “inversion” in DIP refers specifically to this: in the naive design, the high-level module depends on the low-level module. After DIP, the low-level module depends on the high-level module’s interface. The dependency arrow is literally inverted. This only happens when the interface lives in the consumer’s domain.

DIP in the B2B order platform: a before/after

Before DIP, the B2B platform had this structure:

  • billing/ imports order/infra/postgres-order-repo.ts → concrete-to-concrete
  • billing/ imports payment/stripe-gateway.ts → concrete-to-concrete
  • order/ imports db/postgres-driver.ts → concrete-to-concrete

Every module could break every other module through implementation changes. Adding a new payment provider required changing billing. Swapping the database required touching everything.

After DIP:

  • billing/ports/IOrderPort.ts — owned by billing; defines getConfirmedOrder
  • billing/ports/IPaymentPort.ts — owned by billing; defines chargeCustomer
  • order/adapters/BillingOrderAdapter.ts — implements IOrderPort, lives in order module but depends on billing’s interface
  • payment/adapters/StripePaymentAdapter.ts — implements IPaymentPort, lives in payment module but depends on billing’s interface

Billing now has zero compile-time knowledge of the order module’s internals or the payment module’s internals. The order and payment modules depend on billing’s boundary definitions. Adding a new payment provider means writing a new adapter that implements IPaymentPort — billing itself does not change.

lesson.inset.note

A common confusion: DIP does not mean “always use an interface for everything.” It means that at the significant architectural boundaries — where a high-level policy module needs to cross into low-level mechanism territory — the interface should be owned by the high-level side. For internal details within a module, or for stable, third-party infrastructure libraries, the overhead of interface inversion may not be worth it. DIP is a tool for architectural boundaries, not a universal code style rule.

Quiz

The order module publishes an interface `IOrderRepository` in its own package: `order/interfaces/IOrderRepository.ts`. The billing module imports and uses it. The order team later refactors, splitting the interface into `IOrderReader` and `IOrderWriter`. Billing breaks. What went wrong from a DIP perspective?

Quiz

The billing module defines IOrderPort in its own package. At compile time, BillingService.ts only imports IOrderPort. At runtime, a dependency injection container wires IOrderPort to PostgresOrderRepository. A new developer says: 'BillingService still calls the PostgreSQL database at runtime — DIP didn't actually remove the dependency.' How do you respond?

Quiz

In hexagonal architecture (which you will see in unit 04), the domain defines 'ports' — interfaces for all outbound dependencies. The domain module owns these ports. Infrastructure adapters implement them. Which statement explains why hexagonal architecture is structurally just DIP applied at the architectural layer?

Recall before you leave
  1. 01
    What are the two statements of DIP, and why does the second one (about abstractions not depending on details) matter as much as the first?
  2. 02
    In the B2B platform, where exactly should IOrderPort live, and why does its package location determine who can change it?
  3. 03
    What is the 'compile-time boundary' that DIP draws, and how does a dependency injection container relate to it?
Recap

The Dependency Inversion Principle has two halves. The first: high-level modules (business policy) should not depend on low-level modules (technical mechanism) — both should depend on abstractions. The second: those abstractions should be owned by the high-level side, not the low-level side.

Interface ownership is the operational consequence. An interface defined in the low-level module’s package is still owned by the low-level module — the provider can change it, split it, move it, and break every consumer. An interface defined in the high-level module’s package is owned by the consumer — it changes only when the consumer’s requirements change, and the provider must conform to the consumer’s contract.

The structural result: after DIP, the source-code dependency arrow from the low-level module points toward the high-level module’s interface, not away from it. The provider depends on the consumer’s boundary definition. This is the actual inversion.

This mechanism is what makes hexagonal architecture (unit 04) and clean architecture (unit 05) work structurally. In hexagonal architecture, the domain owns its ports — every outbound interface is defined by the domain. Infrastructure adapters implement those ports and therefore depend inward. In clean architecture, the dependency rule runs toward the center — inner circles define the interfaces, outer circles implement them. DIP is not an architectural pattern in itself; it is the underlying mechanism that all those patterns rely on to enforce their dependency boundaries at compile time.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 4 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.