Lesson 5 of 5

Dependency Inversion Principle

A wall socket doesn't care whether you plug in a lamp, a laptop charger, or a vacuum cleaner — it just exposes a standard shape, and anything built to that standard shape works. The electrician who wired the wall never needed to know about your specific vacuum cleaner. That's the Dependency Inversion Principle (DIP): high-level code should depend on abstractions, not on low-level, concrete implementations — and those abstractions shouldn't depend on the details either. It's the "D" that ties the whole SOLID set together.

💡 The Bottom Line

Don't let your important business logic hard-wire itself to one specific database, one specific email service, or one specific payment gateway. Depend on an interface instead — and let the concrete choice be plugged in from outside. Swapping implementations should never require editing the business logic that uses them.

The trap: high-level code wired directly to low-level details

class MySQLDatabase {
  save(order) { /* talks directly to MySQL */ }
}

class OrderService {
  constructor() {
    this.db = new MySQLDatabase(); // hard-wired to one specific database
  }
  placeOrder(order) {
    // ...business logic...
    this.db.save(order);
  }
}

OrderService is supposed to be high-level business logic — but it's welded directly to a low-level detail (MySQL). Switching to PostgreSQL, or writing a test that shouldn't touch a real database at all, both require editing OrderService itself.

⚠️ Watch Out

"Dependency Inversion" is often confused with "Dependency Injection" — they're related but not the same thing. Dependency Injection (passing a dependency in via a constructor or parameter, instead of creating it internally) is the common technique used to achieve Dependency Inversion (the design goal of depending on abstractions). You can inject a concrete class directly and still violate DIP if there's no abstraction in between.

Fixing it: introduce an abstraction, inject the detail

class Database {
  save(order) { throw new Error("must implement save()"); } // the abstraction
}

class MySQLDatabase extends Database {
  save(order) { /* talks to MySQL */ }
}

class PostgresDatabase extends Database {
  save(order) { /* talks to Postgres */ }
}

class OrderService {
  constructor(db) {          // depends on the abstraction, injected from outside
    this.db = db;
  }
  placeOrder(order) {
    // ...business logic, unchanged no matter which db is passed in...
    this.db.save(order);
  }
}

const service = new OrderService(new PostgresDatabase()); // swap freely from outside

OrderService now only knows about the shape of Database, never about MySQL or Postgres specifically. Swapping databases — or handing it a fake, in-memory database for a unit test — never requires touching OrderService at all.

🙋 There Are No Dumb Questions

Q: What does "inversion" refer to, exactly?
A: In naive designs, high-level modules (business logic) depend on low-level modules (database, network, filesystem code) — the dependency arrow points "downward" toward details. DIP inverts that arrow: both high-level and low-level code depend on a shared abstraction sitting between them, and the low-level code is the one that has to conform to it — not the other way around.

Why the whole SOLID set leans on this one

DIP is what makes Open/Closed actually achievable in practice: you can only "extend without modifying" if the high-level code was written against an abstraction in the first place. It's also why untested-feeling code is so often DIP-shaped — logic hard-wired to a real database or a real HTTP client can't be tested without one, while logic written against an abstraction can be tested against a lightweight fake instead.

🔧 Try It Yourself

A ReportGenerator class creates a new EmailSender() directly inside its constructor to send finished reports. Sketch how you'd change it so ReportGenerator depends on a NotificationSender abstraction instead — and can be handed an EmailSender, an SlackSender, or a fake test sender, all without changing ReportGenerator itself.

✅ Quick Check

A PaymentProcessor class directly instantiates new StripeClient() inside itself. What does the Dependency Inversion Principle suggest?


That's all five. Time for the recap — and a look at how these five principles reinforce each other in real, everyday code.

← Back to
Next →