<arch.design/>
Principles/Dependency Inversion Principle
{ }CodeArchitectureintermediate1994solidmartininversion-of-controlabstraction

Dependency Inversion Principle

High-level modules should not depend on low-level modules — both should depend on abstractions, inverting the conventional dependency direction.

4/5
{ }
Operates at: Code level

Inside a codebase — classes, modules, files

How it works

The Dependency Inversion Principle (DIP) is the 'D' in SOLID (Robert C. Martin, 1994). It has two statements: (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.

In a conventional layered design, the OrderService (high-level) depends on PostgresDatabase (low-level) directly. If the database changes, the service must change. DIP inverts this: define an OrderRepository interface, have the service depend on the interface, and have PostgresDatabase implement it. Now the high-level module owns the interface — the low-level module depends on the high-level abstraction, not the other way around.

This is the principle that underlies Dependency Injection, Hexagonal Architecture, and the Repository pattern. DIP is about ownership of the abstraction: the interface belongs to the high-level policy layer, not the low-level detail layer.

Implementation

TypeScript · Go · Rust
// ❌ High-level OrderService directly depends on low-level PostgresDB
class PostgresDB {
  save(order: Order): void { /* SQL INSERT */ }
}

class OrderService {
  private db = new PostgresDB(); // ❌ concrete dependency — impossible to test or swap

  placeOrder(order: Order): void {
    this.db.save(order);
  }
}

// ✓ Both depend on an abstraction — the interface belongs to the high-level layer
interface OrderRepository {
  save(order: Order): Promise<void>;
  findById(id: string): Promise<Order | null>;
}

class OrderService {
  constructor(private repo: OrderRepository) {} // depends on abstraction ✓

  async placeOrder(order: Order): Promise<void> {
    await this.repo.save(order);
  }
}

// Low-level module implements the high-level abstraction
class PostgresOrderRepository implements OrderRepository {
  async save(order: Order)              { /* SQL INSERT */ }
  async findById(id: string)            { /* SQL SELECT */ return null; }
}

// Test double — zero infrastructure required
class InMemoryOrderRepository implements OrderRepository {
  private orders = new Map<string, Order>();
  async save(o: Order)       { this.orders.set(o.id, o); }
  async findById(id: string) { return this.orders.get(id) ?? null; }
}

Why it matters

High-level business logic is the most valuable code in a system. When it directly imports low-level infrastructure (databases, HTTP clients, email providers), it becomes fragile and hard to test. DIP keeps the valuable code stable by letting infrastructure adapt to it, not the other way around.

When to use

  • Any time a high-level module interacts with infrastructure (database, email, API, queue)
  • When you want to unit-test business logic without spinning up infrastructure
  • When the infrastructure provider might change (moving from AWS to GCP, MySQL to Postgres)
  • In combination with DI containers that resolve abstractions to implementations

When NOT to use

  • Low-level utility code (parsers, formatters) where no abstraction is needed
  • When the dependency is stable and will never change (standard library functions)

Trade-offs

+

High-level policy is decoupled from low-level details — each changes independently

Requires defining and maintaining interfaces — more files, more surface area

+

Business logic can be tested without real infrastructure

Misapplied DIP adds interfaces where none are needed, increasing complexity

+

Infrastructure can be replaced transparently

Requires a wiring layer (DI container or manual factory) to connect abstractions to implementations

In production

Spring Framework

Every Spring bean is accessed through an interface; the container inverts control and resolves dependencies at startup

NestJS

Providers registered by interface token — @Inject() receives an abstraction; the module wires the concrete class

Clean Architecture apps

Use case layer defines port interfaces; infrastructure layer implements them — dependency arrows always point inward

Industry adoption

4/5Widely adopted — mainstream at medium-to-large engineering orgs.

Related principles