Watch the arrow flip
Initially OrderService points directly to MySqlDatabase. Press Invert to insert an abstraction layer and flip the dependency.
Code example
OrderService directly instantiating MySqlDatabase vs. injecting an IDatabase abstraction. Toggle to compare languages.
// ? BAD — high-level OrderService depends on low-level MySqlDatabase
public class OrderService
{
private readonly MySqlDatabase _db; // concrete dependency!
public OrderService()
{
_db = new MySqlDatabase(); // hard-wired — can't swap to Postgres
}
public void SaveOrder(Order o) => _db.Insert(o);
}
// ? GOOD — both depend on IDatabase abstraction
public interface IDatabase // the abstraction
{
void Insert(Order o);
}
public class MySqlDatabase : IDatabase // detail depends on abstraction
{
public void Insert(Order o) { /* MySQL specific */ }
}
public class PostgresDatabase : IDatabase // swap in without touching OrderService
{
public void Insert(Order o) { /* Postgres specific */ }
}
public class OrderService // high-level, depends on abstraction
{
private readonly IDatabase _db; // interface — not concrete class
public OrderService(IDatabase db) // injected from outside
{
_db = db;
}
public void SaveOrder(Order o) => _db.Insert(o);
}
// Wired up at composition root (startup / DI container)
var service = new OrderService(new MySqlDatabase());
// Or swap to Postgres with zero changes to OrderService:
var service = new OrderService(new PostgresDatabase());
// ? BAD — high-level service creates its own low-level dependency
class OrderService {
constructor() {
this._db = new MySqlDatabase(); // hard-wired!
}
saveOrder(order) { this._db.insert(order); }
}
// ? GOOD — inject the dependency, depend on the interface
class MySqlDatabase {
insert(order) { console.log('MySQL insert', order); }
}
class PostgresDatabase {
insert(order) { console.log('Postgres insert', order); }
}
class InMemoryDatabase { // for testing — no real DB needed
constructor() { this.records = []; }
insert(order) { this.records.push(order); }
}
class OrderService {
constructor(db) { // receive the abstraction
this._db = db;
}
saveOrder(order) { this._db.insert(order); }
}
// Production
const prod = new OrderService(new MySqlDatabase());
// Test — no database, no network, instant
const test = new OrderService(new InMemoryDatabase());
Key concepts
Dependency Inversion
Neither high-level nor low-level modules depend directly on each other. Both point at a shared abstraction in the middle.
Abstraction Layer
An interface sits between layers. High-level code calls it without knowing the implementation. Low-level code implements it without knowing who calls it.
Dependency Injection
Don't let a class create its own dependencies. Inject them through the constructor (or a DI container) so they can be swapped — including in tests.
Inversion of Control
Control over which implementations are used moves from the class to the caller. Frameworks, DI containers, and plug-in systems are all IoC in practice.