Dependency Inversion
Making important code depend on an abstraction — an interface describing what it needs — instead of on one specific, concrete implementation.
What is it?
Imagine an OrderService class that, deep inside one of its methods, creates a new PostgresDatabase() and calls .save() on it directly. That looks harmless, but it means OrderService is now permanently welded to Postgres. Want to write a fast unit test for the order logic? You can't, without a real Postgres connection. Want to switch to a different database, or mock storage for a test? You have to go edit OrderService itself, even though none of its actual logic changed.
Dependency inversion flips this relationship around. Instead of OrderService depending directly on the concrete PostgresDatabase class, it depends on an abstraction — an interface like OrderRepository that just describes "something with a .save(order) method." The concrete PostgresDatabase class then implements that interface. Now OrderService doesn't know or care whether it's talking to Postgres, an in-memory fake, or something else entirely — it only knows about the shape it depends on.
The word "inversion" refers to which direction the dependency points: normally you'd think the high-level business logic depends on the low-level database detail. Dependency inversion makes both the high-level logic and the low-level detail depend on a shared abstraction sitting between them, instead of the high-level logic depending directly on the low-level one.
Explain like I'm 10
A lamp doesn't need to know whether it's plugged into a coal power plant, a solar panel, or a wind turbine — it just needs a standard wall socket. The socket is the abstraction; any power source that fits it can be swapped in without rewiring the lamp.
Examples
Before: business logic depends on a concrete class
class PostgresDatabase {
save(order) {
// real SQL insert
}
}
class OrderService {
constructor() {
this.db = new PostgresDatabase(); // hard-wired to one concrete class
}
placeOrder(order) {
// ...business rules...
this.db.save(order);
}
}
// Testing this requires a real (or heavily mocked) Postgres connection.
const service = new OrderService();OrderService creates and depends on PostgresDatabase directly. There's no way to substitute a fake for testing without editing OrderService's source code.
After: business logic depends on an abstraction
// The abstraction both sides depend on
class OrderRepository {
save(order) { throw new Error("not implemented"); }
}
class PostgresOrderRepository extends OrderRepository {
save(order) { /* real SQL insert */ }
}
class InMemoryOrderRepository extends OrderRepository {
constructor() { super(); this.orders = []; }
save(order) { this.orders.push(order); }
}
class OrderService {
constructor(repository) {
this.repository = repository; // depends on the abstraction, injected from outside
}
placeOrder(order) {
// ...business rules...
this.repository.save(order);
}
}
// Production
const service = new OrderService(new PostgresOrderRepository());
// Test — no real database needed
const testService = new OrderService(new InMemoryOrderRepository());OrderService no longer knows Postgres exists. In tests, an InMemoryOrderRepository is swapped in with no changes to OrderService itself, and the business logic can be verified in isolation.
How it works
Dependency inversion has two halves that are easy to conflate but worth separating: (1) defining an abstraction (an interface, or in JavaScript often just an agreed-upon set of methods) that captures only what the high-level code actually needs, and (2) injecting the concrete implementation from outside, typically through a constructor or function parameter, rather than having the high-level code instantiate it itself. That second half — injection — is what actually makes the swap possible; defining an abstraction alone doesn't help if the code still hard-codes which implementation to construct.
Why does it exist?
It exists to solve two costly problems at once: untestable business logic (because it's welded to a real database, real network calls, or real filesystem access) and rigid systems (because switching a storage engine, a payment provider, or a third-party library means rewriting the core logic that used it directly, instead of just writing a new implementation of an existing abstraction).
When to use it
Apply it at the boundaries where your core logic meets something volatile, slow, or hard to test in isolation — databases, third-party APIs, the filesystem, the current time, randomness. Anywhere you find yourself wanting to write a test but being blocked by "well, that would need a real X" is a strong signal dependency inversion belongs there.
When not to use it
Introducing an abstraction and injected dependency for something that will only ever have one implementation, and isn't a pain point for testing (e.g., a pure math utility), adds a layer of indirection that buys nothing. Reserve it for genuine seams — places where substitution is plausible or testing is otherwise blocked.
Common mistakes
Creating an interface for every single class 'for flexibility,' even ones that will only ever have one implementation and don't need to be swapped.
Defining the abstraction from the low-level implementation's point of view (mirroring exactly what Postgres offers) instead of from what the high-level logic actually needs.
Still constructing the concrete dependency inside the high-level class (e.g.,
new PostgresOrderRepository()insideOrderService's constructor) — which recreates the original tight coupling even though an interface now exists.
Practice exercises
- Easy:
Take a class that directly calls
fetch()to get weather data, and change it to depend on an injectedWeatherClientabstraction instead. - Medium:
Write a fake, in-memory implementation of an
EmailSenderabstraction so that aNotificationService's logic can be unit tested without sending real emails. - Hard:
Design an abstraction for a
PaymentGatewayused by a checkout service, so that swapping between two real payment providers (and a test fake) requires zero changes to the checkout service's code. List the methods your abstraction needs.
Interview questions
What is dependency inversion?
A principle where high-level code depends on an abstraction (like an interface) rather than a concrete, low-level implementation — and the concrete implementation also depends on that same abstraction, inverting the naive direct dependency.
How does dependency inversion improve testability?
By letting a test substitute a fake or in-memory implementation of the abstraction in place of the real one (e.g., a real database), so business logic can be exercised in isolation, quickly and deterministically.
What's the difference between dependency inversion and dependency injection?
Dependency inversion is the design principle (depend on abstractions, not concretions); dependency injection is the specific technique of supplying a dependency from outside — like through a constructor parameter — which is what makes the inversion practically possible.