Intermediate16 min readSOLID Principleslive prototype

Dependency Inversion Principle (DIP)

The 'D' in SOLID: don't let your important high-level code reach down and grab a specific tool by name. Have both the high-level code and the tool agree on an interface, and plug the tool in from outside. Now you can swap the tool — or fake it in a test — without touching the code that matters.

The idea

What it is

The Dependency Inversion Principle is the D in SOLID. Its job is to stop your most important code — the high-level policy, the part that holds the business rules — from being glued to a specific tool it happens to use. Instead of the policy reaching down and naming a concrete tool, you put an interface in the middle and let both sides depend on that interface.

Picture the power socket in your wall. Your laptop charger doesn't depend on the power plant — it depends on the socket standard. As long as something behind the wall delivers that standard, the charger neither knows nor cares whether the electricity comes from coal, a nuclear plant, or the solar panels you just installed. You can swap the entire power source and never touch your charger. DIP asks you to design code the same way: depend on the socket (an interface), not on the plant (a concrete class).

The one sentence to remember

High-level policy and low-level details should both depend on an abstraction (an interface) — not on each other. The policy owns the socket; the details plug into it.

DIP is not Dependency Injection

These get mixed up constantly. DIP is the principle — a goal about which way your dependencies should point (toward abstractions). Dependency Injection (DI) is just one technique for reaching that goal: instead of a class building its own dependency with new, you hand the dependency to it from outside (usually through the constructor). You can follow DIP without a DI framework, and you can use DI without truly following DIP. DIP = the why; DI = one how.

Mechanics

How it works

The two formal rules

Robert C. Martin states DIP as two rules. They sound abstract; we'll make them concrete in a moment.

  • Rule 1 — High-level modules should not depend on low-level modules. Both should depend on abstractions. (Your OrderService shouldn't depend on MySQLDatabase; both should depend on an OrderRepository interface.)
  • Rule 2 — Abstractions should not depend on details. Details should depend on abstractions. (The interface shouldn't mention MySQL-specific types; instead, MySQLOrderRepository is the one that bends to fit the interface.)

The classic smell: a service that `new`s its own tool

Here is the trap, and almost everyone writes it at first. A high-level class builds the concrete tool it needs inside itself:

typescript
class OrderService {            // high-level policy
  private email = new EmailSender();   // ⚠️ welded to a concrete detail

  placeOrder(order: Order) {
    // ...business rules...
    this.email.send(order.customer, "Your order is confirmed");
  }
}

That single new EmailSender() line quietly causes two real problems. You can't swap it — the day product wants SMS instead of email, you have to open OrderService and edit business-critical code just to change a delivery channel. And you can't test it — every test of OrderService now fires a real email, because the service insists on building the real sender. The policy has been welded to a detail it should never have cared about.

The fix: depend on an interface, inject the detail

Introduce an interface that describes what the policy needsMessageSender with a send(...) method — and have OrderService hold one of those instead of a concrete sender. Then pass the real implementation in from outside, through the constructor:

typescript
interface MessageSender {                // the abstraction (the "socket")
  send(to: string, text: string): void;
}

class OrderService {
  constructor(private sender: MessageSender) {}  // injected from outside

  placeOrder(order: Order) {
    // ...business rules...
    this.sender.send(order.customer, "Your order is confirmed");
  }
}

class EmailSender implements MessageSender { /* ... */ }   // a detail
class SMSSender   implements MessageSender { /* ... */ }   // another detail

new OrderService(new EmailSender());   // wire it up at the edge of the app

Where the "inversion" actually is

The name confuses people, so look closely at the arrows. Before, the dependency arrow ran downward: policy → detail (OrderServiceMySQLDatabase). The high-level code pointed at, and was at the mercy of, the low-level code. After, that arrow is inverted: the detail now points up at the abstraction (MySQLOrderRepositoryOrderRepository), and the policy points at the same abstraction. The crucial twist is that the interface is owned by the high-level side — it's defined in terms of what the policy needs, and the detail must conform to it. The detail now serves the policy, not the other way around. That reversal of who-conforms-to-whom is the 'inversion'.

BEFORE — welded AFTER — inverted OrderService high-level policy depends on MySQLDatabase low-level detail OrderService high-level policy «interface» OrderRepository implements ↑ MySQLOrderRepository
Before, the arrow runs policy → detail: OrderService points straight down at the concrete MySQLDatabase (filled arrowhead = a hard dependency). After, an «interface» OrderRepository sits in the middle. OrderService depends on it, and MySQLOrderRepository implements it — so the detail's arrow is now inverted, pointing up (open arrowhead) toward the abstraction the policy owns. Same work gets done; the dependency now flows the other way.

Why this is the whole point

  • Swapping — to go from email to SMS you write a new SMSSender implements MessageSender and change one wiring line at the edge of the app. OrderService is never opened.
  • Testing — in a unit test you inject a FakeSender that just records what it was asked to send. The test is fast, offline, and asserts on the recording — no real email, no real database.
  • Decoupling — the policy and the detail no longer know each other's names. Either can change independently as long as the interface holds. That's loose coupling, and it's what keeps a large codebase soft enough to change.

Who owns the interface matters

DIP isn't just 'use an interface somewhere'. The abstraction should be defined by, and live with, the high-level policy — it describes what the policy needs (MessageSender.send), in the policy's language. The low-level detail then conforms to it. If instead the interface mirrors a specific tool's API, you've only added indirection, not inverted anything.

Interactive prototype

See it. Build it. Break it.

A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.

About this simulation

Two modes of the same OrderService. In Welded, the service news an EmailSender inside itself — the dependency arrow points straight down at the concrete class (in danger red), and the side panel shows swap? ✗ and testable? ✗. Flip to Inverted and an «interface» MessageSender slides in between them; now a tray lets you plug in EmailSender, SMSSender, or FakeSender (test). Each pick inverts the bottom arrow to point up at the interface, flips both indicators to green ✓, and the fixed panel narrates the swap — OrderService never changed. One panel, replaced each click; nothing scrolls away.

Hands-on

Try these yourself

Open the prototype above, predict what happens, then verify.

try 01

Feel the weld

Open the prototype in Welded mode. The OrderService box literally contains new EmailSender(), and a red dependency arrow points straight down at the concrete EmailSender. Read the side panel: swap? ✗, testable? ✗. Notice the tray of other senders is disabled — there is no way to plug anything else in, because the service builds its own. This is the smell DIP fixes.

try 02

Invert the arrow

Flip to Inverted mode. Watch an «interface» MessageSender slide in between the service and the implementations, and watch the bottom arrow invert to point up at the interface. Both indicators flip to green: swap? ✓, testable? ✓. Nothing inside OrderService changed — it now depends on the socket, not the plant.

try 03

Swap and fake without touching the service

In Inverted mode, plug in SMSSender from the tray — the explain panel confirms OrderService never changed. Now plug in FakeSender (test) and read why that makes the service testable: the fake just records the message, so a unit test runs with no real email or network. Click between the three implementations and confirm the service box stays identical every time — that is the payoff of inverting the dependency.

In practice

When to use it — and what trips people up

Invert the dependencies that are likely to change

DIP earns its keep around volatile or external dependencies — anything that talks to the outside world or that you might reasonably want to replace, fake, or reconfigure. These are exactly the things that make code hard to test and hard to evolve when you new them inline.

  • Databases / persistenceOrderRepository interface in front of MySQL, Postgres, or an in-memory store for tests.
  • Network & third-party services — payment gateways, email/SMS providers, an HTTP client to another team's API.
  • The clock and randomness — wrap now() and random() behind an interface so tests can pin time and seeds instead of being flaky.
  • Anything you want to fake in a unit test — if you can't test a class without standing up real infrastructure, that dependency wants inverting.

Don't invert stable, never-changing dependencies

DIP is a tool, not a tax on every line. Don't wrap the standard library, simple value objects (a Money, a Point), String, or math functions behind interfaces — they don't vary, you'll never fake them, and you'll never swap them. Inverting a stable dependency just adds an extra hop and an empty interface that confuses the next reader. Invert what is volatile; depend directly on what is stable.

What it gives you

  • Testability — inject a fake or mock and unit-test the policy with no database, network, or email in sight.
  • Swappability — change Email→SMS, MySQL→Postgres, or real→stub by editing one wiring line; the high-level code is never reopened.
  • Decoupling — policy and details stop knowing each other's names, so each can evolve independently behind a stable interface.
  • Parallel work & boundaries — once the interface is agreed, two people can build the policy and the implementation at the same time.

Common mistakes

  • More moving parts — every inverted dependency adds an interface plus a concrete class, which is more to read and navigate.
  • Wiring overhead — something must construct and connect the pieces at the app's edge (a main()/composition root or a DI container), and that wiring can sprawl.
  • Indirection cost — 'go to definition' lands on the interface, not the real code, which can slow down tracing a call for the first time.
  • Over-application — inverting stable or one-off dependencies adds ceremony with no payoff; DIP misused turns simple code into interface soup.

Reference

Code & further reading

A minimal reference implementation and pointers worth bookmarking.

// BEFORE — OrderService is welded to a concrete EmailSender.
class EmailSenderV0 {
  send(to: string, text: string) { /* ...real email... */ }
}
class OrderServiceV0 {
  private email = new EmailSenderV0();   // ⚠️ can't swap, can't test
  placeOrder(customer: string) {
    this.email.send(customer, "Order confirmed");
  }
}

// AFTER — depend on an interface, inject the detail from outside.
interface MessageSender {
  send(to: string, text: string): void;
}
class OrderService {
  constructor(private sender: MessageSender) {}   // injected
  placeOrder(customer: string) {
    this.sender.send(customer, "Order confirmed");
  }
}

// Swap Email -> SMS by changing ONE wiring line. OrderService untouched.
class EmailSender implements MessageSender {
  send(to: string, text: string) { /* ...real email... */ }
}
class SMSSender implements MessageSender {
  send(to: string, text: string) { /* ...real SMS... */ }
}
const prod = new OrderService(new SMSSender());   // was new EmailSender()

// Inject a FAKE in a test — fast, offline, asserts on a recording.
class FakeSender implements MessageSender {
  sent: { to: string; text: string }[] = [];
  send(to: string, text: string) { this.sent.push({ to, text }); }
}
const fake = new FakeSender();
new OrderService(fake).placeOrder("ada@x.io");
// expect(fake.sent[0].text).toBe("Order confirmed");

References & further reading

5 sources

Knowledge check

Did it land?

Quick questions, answers revealed on submit. Sign in to save your best score.

question 01 / 05

What does the Dependency Inversion Principle actually say?

question 02 / 05

An OrderService contains private email = new EmailSender(). Which two problems does this directly cause?

question 03 / 05

What is the difference between the Dependency Inversion Principle and Dependency Injection?

question 04 / 05

After applying DIP, what does the 'inversion' refer to — what changed about the dependency arrow?

question 05 / 05

Which dependency is the BEST candidate to leave alone rather than invert behind an interface?

0/5 answered