Overview
The Dependency Inversion Principle (DIP) says high-level policy should not depend on low-level details; both should depend on abstractions, and the abstractions should be owned by the high-level side. OrderService should depend on a PaymentGateway interface it defines, not on StripeClient directly.
DIP 'inverts' the usual direction: the database or vendor module depends on the business module's interface, not the other way around. This makes business logic testable with fakes, lets infrastructure be swapped, and is the foundation of clean and hexagonal architecture. Dependency injection is the usual technique for supplying implementations.
Your lamp does not get wired directly into the building's electrical system. Both the lamp and the building agree on a standard socket (abstraction), so either side can change independently.
When to use it
- Business logic calls databases, networks, clocks, or vendors.
- You need unit tests without real infrastructure.
- Infrastructure may change (database, cloud provider, vendor).
Where it shows up in interviews
Recognize it when: unit tests need a database or network.
- Design an order service
- Design a notification service
Recognize it when: support several providers.
- Design a payment gateway
- Design file storage for local and cloud
Where it is used in real software
Constructor injection of interfaces is the default style; containers wire implementations.
Core defines ports; adapters for databases and APIs implement them.
java.time.Clock is injected so time-dependent logic can be tested deterministically.
Key terms
- High-level module
- Business policy (OrderService).
- Low-level module
- Details (Postgres repository, HTTP client).
- Abstraction ownership
- The interface lives with the high-level module.
- Dependency injection
- Passing dependencies in rather than creating them.
- Composition root
- The one place where concrete classes are wired together.
How it works, step by step
- 1Find direct dependencies on details
new StripeClient() inside business code.
- 2Define an interface in the business layer
PaymentGateway.charge().
- 3Implement it in the infrastructure layer
StripePaymentGateway.
- 4Inject through the constructor
OrderService(gateway).
- 5Wire at the composition root
main(), DI container, or module setup.
Dependency direction
OrderService needs to charge cards and save orders
| Aspect | Without DIP | With DIP |
|---|---|---|
| OrderService imports | StripeClient, PgPool | PaymentGateway, OrderRepository (own interfaces) |
| Unit test | Needs Stripe sandbox + database | In-memory fakes |
| Switch to Adyen | Edit OrderService | New adapter class |
| Dependency arrow | Business -> infrastructure | Infrastructure -> business interfaces |
NOWAspect: OrderService imports | Without DIP: StripeClient, PgPool | With DIP: PaymentGateway, OrderRepository (own interfaces)
Business code becomes the stable center; details plug into it.
Implementation
// Business layer owns the abstractionsexport interface PaymentGateway { charge(customerId: string, cents: number): Promise<string> }export interface OrderRepository { save(order: { id: string; paymentId: string }): Promise<void> }export interface Clock { now(): Date } export class OrderService { constructor(private payments: PaymentGateway, private orders: OrderRepository, private clock: Clock) {} async place(id: string, customerId: string, cents: number) { if (this.clock.now().getUTCHours() === 3) throw new Error("Maintenance window"); const paymentId = await this.payments.charge(customerId, cents); await this.orders.save({ id, paymentId }); return paymentId; }} // Test with fakes: no network, no database, deterministic timeconst saved: unknown[] = [];const service = new OrderService( { charge: async () => "pay_1" }, { save: async (o) => { saved.push(o); } }, { now: () => new Date("2026-01-01T10:00:00Z") },);Complexity and performance
No infrastructure.
Small cost.
Trade-offs
Interfaces for everything clutter code; apply DIP at boundaries with infrastructure or volatile modules.
DI frameworks can hide wiring; constructor injection keeps dependencies explicit.
Variants and related techniques
Wire objects in main without a framework.
Objects fetch dependencies from a registry; considered an anti-pattern because dependencies are hidden.
Common mistakes
- Interfaces defined next to the implementation in the infrastructure layer.
Fix: The consumer (business layer) should own the interface.
- new-ing dependencies inside business classes.
Fix: Inject them.
- Using system time and randomness directly.
Fix: Inject Clock and random sources.
Interview questions
DIP vs dependency injection?
DIP is the principle that high-level code depends on abstractions it owns rather than on details. Dependency injection is a technique for supplying those implementations from outside, which makes DIP practical.
Why is DIP important for testing?
Business logic depends on interfaces, so tests can pass fast, deterministic fakes for databases, payment providers, and clocks.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| Refactor a service that calls new Date() and an SDK | Easy | Inject Clock and gateway. |
| Design ports and adapters for a booking service | Medium | Ownership of interfaces. |