open atlas
↑ Back to track
Code patterns & craft CP · 06 · 02

Dependency inversion

High-level policy must not depend on low-level detail; both depend on an abstraction the policy owns. Flip the arrow — OrderService → OrderRepository ← MySqlOrderRepository — so business logic is testable and infrastructure swappable.

CP Senior ◷ 20 min
Level
FoundationsJuniorMiddleSenior

Your OrderService — the code that holds the actual rules of the business, what an order is, when it can be placed, how it’s priced — opens with import { MySQLDatabase } from "../db/mysql". Read that line again. The most valuable, most stable code in your system reaches down and takes a hard dependency on a specific database driver. Now the rules can’t be tested without a running MySQL, can’t move to Postgres without surgery, and can’t even be reasoned about without dragging connection pools into your head.

Something is backwards. The thing that changes least often (your domain policy) is chained to the thing that changes most often (a vendor-specific I/O detail). Dependency inversion is the rule that puts the arrow the right way round — and the surprising part is who owns the abstraction once you do.

Goal

After this lesson you can state the Dependency Inversion Principle precisely (both halves), explain why the dependency arrow should point toward the stable policy rather than the volatile detail, refactor a Service → ConcreteDriver dependency into Service → Interface ← Driver with the interface owned by the policy, and recognise the cargo-cult failure mode where an interface merely mirrors one concrete class and inverts nothing.

1

DIP has two halves, and the second is the one everyone forgets. The principle states: (a) high-level modules should not depend on low-level modules — both should depend on abstractions; and (b) abstractions should not depend on details — details should depend on abstractions. Most people remember half (a) — “code to an interface” — and stop. Half (b) is what gives DIP its teeth: the abstraction is shaped by what the policy needs, not by what some concrete class happens to offer.

// high-level policy (stable: changes when the business changes)
class OrderService { /* what an order is, pricing, placement rules */ }

// low-level detail (volatile: changes when infra/vendors change)
class MySQLDatabase { /* connection pools, SQL, driver quirks */ }

The question DIP answers is: which way does the import go between these two?

2

Naively, the arrow points from policy to detail — and that is the bug. The default, the thing you write without thinking, is for the high-level service to import the low-level driver directly. Now compile-time and run-time dependencies both flow from stable → volatile.

import { MySQLDatabase } from "../infra/mysql";

class OrderService {
  private db = new MySQLDatabase();          // policy nailed to a vendor
  place(order: Order) {
    this.db.query("INSERT INTO orders ...");  // SQL leaking into business code
  }
}

The cost is concrete: you can’t unit-test place() without MySQL; swapping to Postgres edits the class that holds your rules; and a driver upgrade can break business logic. The most valuable code in the system is hostage to the least valuable. That is exactly the dependency direction we want to reverse.

3

Invert it: define an interface the policy needs, owned by the policy. Instead of OrderService → MySQLDatabase, define OrderRepository — an interface phrased in the domain’s language (save(order), findById(id)), not the database’s (query, exec). The service depends on that interface. The concrete MySqlOrderRepository implements it. Crucially, the interface lives with the high-level module, in its package, as part of its contract.

// owned by the policy layer — phrased in domain terms
export interface OrderRepository {
  save(order: Order): Promise<void>;
  findById(id: OrderId): Promise<Order | null>;
}

class OrderService {
  constructor(private orders: OrderRepository) {}   // depends on the abstraction
  async place(order: Order) {
    /* business rules */ await this.orders.save(order);
  }
}

Now both the service and the driver depend on OrderRepository. The arrow from the detail points up toward the policy’s abstraction. That ownership is what makes this “inversion” and not just “an interface somewhere.”

4

The detail now depends on the policy — the arrow is genuinely flipped. MySqlOrderRepository imports OrderRepository (which belongs to the domain) and bends itself to the domain’s vocabulary. The compile-time dependency runs from infrastructure to domain. The volatile thing depends on the stable thing, which is what you want, because stable things make good dependency targets.

import type { OrderRepository } from "../domain/OrderRepository";

export class MySqlOrderRepository implements OrderRepository {
  constructor(private db: MySQLDatabase) {}
  async save(order: Order) {
    await this.db.query("INSERT INTO orders ...", toRow(order));
  }
  async findById(id: OrderId) { /* SQL → Order */ }
}

// composition root wires the volatile detail into the stable policy
const service = new OrderService(new MySqlOrderRepository(mysql));

Tests inject a fake; production injects MySQL; a migration to Postgres is one new class implementing the same interface, with OrderService untouched. Testability and swappability fall out of the inverted arrow.

Worked example

A reporting service, before and after. Before — the policy reaches for the driver:

import { Redis } from "../infra/redis";
import { Postgres } from "../infra/postgres";

class RevenueReport {
  private cache = new Redis();
  private db = new Postgres();
  async monthly(month: string) {
    const hit = await this.cache.get(`rev:${month}`);
    if (hit) return JSON.parse(hit);
    const rows = await this.db.query("SELECT sum(total) ...", [month]); // policy knows SQL
    const total = rows[0].sum;
    await this.cache.set(`rev:${month}`, JSON.stringify(total));
    return total;
  }
}

RevenueReport holds the rule (“monthly revenue is the sum of order totals, cached”) and the mechanics of Redis and SQL. You cannot test the rule without both servers, and the report changes whenever the cache vendor does.

After — invert, with abstractions the report owns:

// domain-owned ports, phrased in the report's language
export interface RevenueStore { sumForMonth(month: string): Promise<number>; }
export interface ReportCache {
  get(key: string): Promise<number | null>;
  set(key: string, value: number): Promise<void>;
}

class RevenueReport {
  constructor(private store: RevenueStore, private cache: ReportCache) {}
  async monthly(month: string) {
    const key = `rev:${month}`;
    const hit = await this.cache.get(key);
    if (hit !== null) return hit;        // pure policy: cache-aside, no vendor in sight
    const total = await this.store.sumForMonth(month);
    await this.cache.set(key, total);
    return total;
  }
}

The cache-aside rule is now testable with two in-memory fakes and zero servers. PostgresRevenueStore and RedisReportCache live in infra and implement these ports. Notice the ports speak the report’s dialect (sumForMonth, not query) — that is the policy owning the abstraction, which is the whole point.

Why this works

Why insist the abstraction be owned by the high-level module and phrased in its language? Because that is the difference between inversion and a sideways indirection. If the interface mirrors the database (query, exec, beginTransaction), the policy still effectively depends on “a SQL database” — you’ve changed the type but not the direction; the domain still bends to infrastructure’s shape. When the interface is OrderRepository.save(order) — defined by what the policy needs, living in the policy’s package — infrastructure must bend to the domain’s shape. The stable module dictates the contract; the volatile module conforms. That ownership is the inversion; the interface keyword alone is not.

Common mistake

The cargo-cult failure mode: an interface that exists only to mirror one concrete class — UserService + IUserService with identical members, MySqlThing + IMySqlThing auto-extracted by the IDE. There is one implementation, the interface changes in lockstep with it, and it’s owned by (and named after) the detail, not the policy. This buys you indirection — an extra file, an extra jump in “go to definition” — and zero decoupling: you still can’t vary the implementation meaningfully, the abstraction leaks the concrete’s vocabulary, and tests gain nothing a real fake wouldn’t. DIP is not “an interface per class.” Invert only where a real seam exists: a boundary you will cross with a second implementation (a test fake counts) or where policy must stay ignorant of a volatile detail. Everywhere else, an interface is just ceremony that raises the cost of reading without lowering the cost of change.

Check yourself
Quiz

A team extracts an interface IOrderRepository whose methods are query(sql), exec(sql), and beginTransaction() — a one-to-one copy of their MySQL wrapper's public API — and has OrderService depend on it. Did they apply Dependency Inversion?

Recap

Dependency inversion says high-level policy and low-level detail must both depend on an abstraction, and that abstraction is owned by the policy and phrased in its language. The default arrow — OrderService → MySQLDatabase — points the wrong way: the stable, valuable code is chained to the volatile, replaceable code. You flip it by defining OrderRepository (a domain port) that OrderService depends on and MySqlOrderRepository implements, so the detail’s dependency arrow now points up toward the policy. The payoff is direct: business logic becomes testable with fakes and infrastructure becomes swappable behind a stable seam. The failure mode to avoid is the interface-per-class cargo cult — a header that mirrors one concrete class, owned by the detail, inverting nothing and buying only indirection. Invert where a real seam exists; everywhere else, skip the ceremony.

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.