SOLID & DESIGN PRINCIPLES / OBJECT DESIGN BRIEF

Dependency Inversion Principle

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.

IntermediatePhase 02 / Topic 5 of 8ResponsibilitiesCollaborationsExtensibility
01

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.

Wall sockets

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.

02

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).
03

Where it shows up in interviews

Testable business logic

Recognize it when: unit tests need a database or network.

  • Design an order service
  • Design a notification service
Swappable infrastructure

Recognize it when: support several providers.

  • Design a payment gateway
  • Design file storage for local and cloud
04

Where it is used in real software

Spring and NestJS

Constructor injection of interfaces is the default style; containers wire implementations.

Hexagonal architecture

Core defines ports; adapters for databases and APIs implement them.

Clock abstraction

java.time.Clock is injected so time-dependent logic can be tested deterministically.

05

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.
06

How it works, step by step

  1. 1
    Find direct dependencies on details

    new StripeClient() inside business code.

  2. 2
    Define an interface in the business layer

    PaymentGateway.charge().

  3. 3
    Implement it in the infrastructure layer

    StripePaymentGateway.

  4. 4
    Inject through the constructor

    OrderService(gateway).

  5. 5
    Wire at the composition root

    main(), DI container, or module setup.

07

Dependency direction

OrderService needs to charge cards and save orders

Step 1 / 4
AspectWithout DIPWith DIP
OrderService importsStripeClient, PgPoolPaymentGateway, OrderRepository (own interfaces)
Unit testNeeds Stripe sandbox + databaseIn-memory fakes
Switch to AdyenEdit OrderServiceNew adapter class
Dependency arrowBusiness -> infrastructureInfrastructure -> 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.

08

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") },);
09

Complexity and performance

Unit test speedMilliseconds

No infrastructure.

Extra types1 interface per external dependency

Small cost.

10

Trade-offs

Indirection

Interfaces for everything clutter code; apply DIP at boundaries with infrastructure or volatile modules.

Container magic

DI frameworks can hide wiring; constructor injection keeps dependencies explicit.

11

Variants and related techniques

Manual (pure) DI

Wire objects in main without a framework.

Service locator

Objects fetch dependencies from a registry; considered an anti-pattern because dependencies are hidden.

12

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.

13

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.

14

Practice problems

ProblemDifficultyWhat it trains
Refactor a service that calls new Date() and an SDKEasyInject Clock and gateway.
Design ports and adapters for a booking serviceMediumOwnership of interfaces.