Dependency inversion principle
A developer ran the test suite on their laptop. 300 real customers received “Your order
is confirmed” at 11:40pm for orders they had never placed. The test was for a discount
calculation. But OrderService created its own SMS client inside itself, so there was no
way to test any part of it without sending real messages to whoever was in the database.
After that, nobody ran those tests. A few weeks later, nobody wrote them either.
OrderService.place() saves an order to MySQL and sends an SMS. How would you write a test for place() that does not need a database and does not send a message? What has to change in OrderService to allow it?
The idea
The formal wording has two halves. High level code should not depend on low level code, both should depend on abstractions. And abstractions should not depend on details.
High level means your business rules: what happens when an order is placed. Low level means the machinery: MySQL, an SMS vendor, the file system. The machinery changes vendors and versions. The business rule should not have to hear about it.
Your phone charger does not know whether the power comes from a coal plant, a dam or the inverter in the hallway. It knows the socket. The power plant does not know about your charger either. It also builds to the socket. Both sides depend on the standard in the middle, and neither depends on the other.
Before: the service builds its own tools
class MySqlOrderStore {
save(orderId: number): string {
return `MySQL: saved order ${orderId}`
}
}
class SmsClient {
send(phone: string, text: string): string {
return `SMS to ${phone}: ${text}`
}
}
class OrderService {
private readonly store = new MySqlOrderStore()
private readonly sms = new SmsClient()
place(orderId: number, phone: string): string[] {
const text = `Order ${orderId} confirmed`
return [this.store.save(orderId), this.sms.send(phone, text)]
}
}
console.log(new OrderService().place(42, '9810012345'))
// > [ 'MySQL: saved order 42', 'SMS to 9810012345: Order 42 confirmed' ]The two new calls inside OrderService are the problem. They weld the business rule to
one database and one vendor. You cannot run place without both, and you cannot swap
either without editing the class that holds your business logic.
After: the service states what it needs
interface OrderStore {
save(orderId: number): string
}
interface Notifier {
send(to: string, text: string): string
}
class OrderService {
constructor(
private readonly store: OrderStore,
private readonly notifier: Notifier,
) {}
place(orderId: number, phone: string): string[] {
const text = `Order ${orderId} confirmed`
return [this.store.save(orderId), this.notifier.send(phone, text)]
}
}
class MySqlOrderStore implements OrderStore {
save(orderId: number): string {
return `MySQL: saved order ${orderId}`
}
}
class SmsNotifier implements Notifier {
send(to: string, text: string): string {
return `SMS to ${to}: ${text}`
}
}
const service = new OrderService(new MySqlOrderStore(), new SmsNotifier())
console.log(service.place(42, '9810012345'))
// > [ 'MySQL: saved order 42', 'SMS to 9810012345: Order 42 confirmed' ]OrderService no longer contains the word MySQL or the word SMS. It declares two
interfaces’ worth of needs in its constructor, and whoever creates it supplies them.
Handing an object what it needs from outside is called dependency injection. In
TypeScript it needs no framework. It is a constructor with parameters.
Now the test from the top of the page:
const saved: number[] = []
const sent: string[] = []
const fakeStore: OrderStore = {
save: (orderId) => {
saved.push(orderId)
return 'saved'
},
}
const fakeNotifier: Notifier = {
send: (to, text) => {
sent.push(`${to}: ${text}`)
return 'sent'
},
}
const underTest = new OrderService(fakeStore, fakeNotifier)
underTest.place(42, '9810012345')
console.log(saved) // [ 42 ]
console.log(sent) // [ '9810012345: Order 42 confirmed' ]No database, no SMS, no mocking library. Two object literals that match the interfaces, which is all structural typing asks for. The test runs in a millisecond and nobody gets a message at 11:40pm.
Why it is called inversion
Flip between the two designs and watch which way the arrows point.
One detail that people miss: the interfaces belong to OrderService. They are named for
what the business needs (OrderStore, Notifier), not for what the vendor offers. If
the interface were called MySqlClient with a runQuery method, the arrows would be
drawn the right way and nothing would really have been inverted.
Where the real classes get created
Something has to call new MySqlOrderStore(). That happens once, at the edge of the
program, in a place often called the composition root: main.ts, or the file that
starts your server.
interface OrderStore {
save(orderId: number): string
}
class MySqlOrderStore implements OrderStore {
save(orderId: number): string {
return `MySQL: saved order ${orderId}`
}
}
class InMemoryOrderStore implements OrderStore {
save(orderId: number): string {
return `Memory: saved order ${orderId}`
}
}
function createStore(environment: 'production' | 'test'): OrderStore {
if (environment === 'production') return new MySqlOrderStore()
return new InMemoryOrderStore()
}
console.log(createStore('test').save(42)) // Memory: saved order 42Everything above that one function works with OrderStore and never learns which one it
got.
You do not inject Math.max or your own Money class. Invert the dependencies that
cross a boundary: the network, the disk, the database, the clock, randomness, another
team’s service. Those are slow, they fail, they cost money, or they change under you.
Plain logic that lives in your own process can be called directly.
Three terms that get mixed up, and you will be asked to separate them. Dependency inversion is the principle: depend on abstractions. Dependency injection is the technique: pass dependencies in, usually through the constructor. An IoC container, like the one in NestJS, is a tool that does the passing for you by reading constructor types. You can follow the principle with plain constructors and no container, which is what this page does.
constructor(private readonly store: OrderStore, ...){ save: (orderId) => ... }const saved: number[] = []environment: 'production' | 'test'function createStore(...): OrderStoreconst fakeStore: OrderStore = { ... }Checkpoint
1. What is the clearest sign in code that a class is breaking dependency inversion?
2. What exactly is inverted?
3. OrderService takes an OrderStore in its constructor. In a test, what is the cheapest thing you can pass?
Dependency inversion says high level business logic should not depend on low level details like a database or an SMS vendor. Both should depend on an abstraction. Concretely, my service does not call new on its database client. It declares an interface for what it needs, takes an implementation through its constructor, and the real classes are created once at the edge of the program, in the composition root. That is dependency injection, and it needs no framework. The inversion is that the database class now depends on an interface owned by the business side, where before the business code depended on it. The payoff is that I can test the service with a fake that is a few lines long and swap vendors without opening the business logic. I only do this for things that cross a boundary, like network, disk and time.
