Dependency inversion vs dependency injection vs IoC
Published
The names are one word apart and both are shortened to "DI", so most people treat them as one thing. They answer two different questions.
Think of a cook in a restaurant kitchen. Dependency inversion is about the recipe: does it say "two spoons of any cooking oil", or "two spoons of one particular brand of sunflower oil"? Dependency injection is about the store room: is the oil handed to the cook, or does the cook leave the kitchen to buy it? A recipe can say "any oil" while the cook still goes shopping. And the store room can hand over exactly one brand to a recipe that only works with that brand.
- Dependency inversion (the D in SOLID) is a principle about types: business code depends on an abstraction, not on a concrete class.
- Dependency injection is a technique about creation: an object is given what it needs (usually through its constructor) instead of creating it.
OrderService has constructor(private gateway: Razorpay). Someone says 'great, that is dependency injection, so we follow the dependency inversion principle'. Are they right? What would you change, if anything?
Two questions, four answers#
Ask the two questions separately about any class and you get four combinations. Only one of them gives you both fast tests and a vendor switch that does not touch business code.
import { Razorpay } from './razorpay'
class OrderService {
private gateway = new Razorpay('key_live_8f2a')
checkout(rupees: number) {
return this.gateway.charge(rupees)
}
}- Unit test with a fake, no networkno
- Switch to Paytm without editing OrderServiceno
- OrderService imports Razorpayyes
The dependency inversion chapter covers why the principle exists and draws the arrows before and after. This page is about telling it apart from the things it gets mixed up with.
Injection without inversion#
This is the most common version in real codebases, because frameworks make injection easy and say nothing about what type you inject:
class Razorpay {
constructor(private key: string) {}
charge(rupees: number) {
return `razorpay ${rupees}`
}
}
class OrderService {
constructor(private gateway: Razorpay) {}
checkout(rupees: number) {
return this.gateway.charge(rupees)
}
}
const fake = { charge: (rupees: number) => `fake ${rupees}` }
new OrderService(fake)
// Error: Property 'key' is missing in typeThe gateway is passed in, so it is injected. But OrderService still names Razorpay. The
fake has the right charge method and TypeScript still rejects it, because Razorpay has a
private field and a class with private members only accepts its own instances (or
subclasses). Your test now has to extend Razorpay and work around its constructor, and a
Paytm can never be passed in at all.
Inversion without injection#
The opposite also exists. Here OrderService names only the interface, but goes and fetches the
real object itself from a shared registry. That is called a service locator:
interface PaymentGateway {
charge(rupees: number): string
}
const registry = new Map<string, PaymentGateway>()
registry.set('payments', { charge: (r) => `razorpay ${r}` })
class OrderService {
// Names only the interface, but fetches the object itself.
private gateway = registry.get('payments')!
checkout(rupees: number) {
return this.gateway.charge(rupees)
}
}
console.log(new OrderService().checkout(300)) // razorpay 300It does depend on an abstraction, and you can swap gateways by changing the registry. The cost
is that the dependency is hidden: new OrderService() takes no arguments, so nothing tells
you it needs a payment gateway until it fails at runtime with nothing registered. Every test has
to remember to fill in the global registry first, and two tests running in parallel can step on
each other's entries. Most teams treat service locator as something to move away from.
Both: the version to aim for#
Ask for the interface in the constructor, and create the real objects in one place at the edge
of the app. That place is often called the composition root: main.ts, the server start
file, or a module's setup function.
interface PaymentGateway {
charge(rupees: number): string
}
class Razorpay implements PaymentGateway {
constructor(private key: string) {}
charge(rupees: number) {
return `razorpay ${rupees}`
}
}
class OrderService {
constructor(private gateway: PaymentGateway) {}
checkout(rupees: number) {
return this.gateway.charge(rupees)
}
}
// main.ts: the one place that knows the real classes
const orders = new OrderService(new Razorpay('key_live_8f2a'))
console.log(orders.checkout(499)) // razorpay 499
// order-service.test.ts: a fake is a plain object
const charged: number[] = []
const fake: PaymentGateway = {
charge: (rupees) => {
charged.push(rupees)
return 'ok'
},
}
new OrderService(fake).checkout(120)
console.log(charged) // [ 120 ]OrderService does not know Razorpay exists. Its constructor lists exactly what it needs. The
test needs no network, no subclass and no global setup.
Where IoC and containers fit#
Two more terms join this pair in interviews.
Inversion of control (IoC) is the broadest idea: instead of your code calling a library, a framework calls your code. Express calls your route handler, React calls your component, a test runner calls your test. The usual summary is "don't call us, we'll call you". Dependency injection is one kind of inversion of control, where control over creating dependencies moves out of the class.
A DI container is a tool that does the wiring in the composition root for you. You register which class to use for each dependency, and it builds the whole graph. NestJS and Angular have one built in, and InversifyJS and tsyringe are standalone ones for TypeScript.
- NestJSBuilt in container
- AngularBuilt in container
- InInversifyJSStandalone, for TypeScript
- tstsyringeStandalone, for TypeScript
| Term | What kind of thing | One line |
|---|---|---|
| Dependency inversion | A principle | Business code depends on abstractions it owns, not on concrete classes. |
| Dependency injection | A technique | An object is handed its dependencies instead of creating them. |
| Inversion of control | A broad idea | The framework calls your code, not the other way round. |
| Service locator | A pattern (usually avoided) | An object fetches its dependencies from a shared registry. |
| DI container | A tool | Builds and wires the objects for you from a list of registrations. |
Using NestJS does not mean you follow dependency inversion. constructor(private razorpay: RazorpayService) in a Nest controller is injected by the container and still names a concrete
class, which is the "injection without inversion" box above. The container solves wiring. Which
type you ask for is still your decision.
You do not need a container to do injection. Wiring by hand in one file is plain code that anyone can read and step through in a debugger. Many teams only reach for a container once the composition root has dozens of services and the hand wiring is a page long.
| Choice | What you gain | What you pay | Pick it when |
|---|---|---|---|
| Constructor injection, wired by hand | Dependencies are visible in the constructor. No magic, no library, easy to debug. | The composition root grows with every service and has to be kept in order. | The default. Most services and libraries, and anything under a few dozen classes. |
| DI container | Builds large object graphs, handles lifetimes (one per app, one per request). | Wiring errors show up at startup instead of at compile time, and there is a framework to learn. | Large apps, and when the framework already has one (NestJS, Angular). |
| Service locator | No constructor changes needed when a dependency is added. | Hidden dependencies, global state in tests, failures at runtime instead of compile time. | Legacy code you are moving away from, one class at a time. |
constructor(private gateway: PaymentGateway) {}private key: string (in a class)new Map<string, PaymentGateway>()registry.get('payments')!const charged: number[] = []In the interview#
Checkpoint#
1. constructor(private gateway: Razorpay) {}. Which is true?
2. What is the main problem with a service locator?
3. Which is the broadest of these ideas?
- Inversion: what type you depend on. Injection: who creates it.
- Injection without inversion is the common one: a concrete class handed in.
- Service locator is inversion without injection, and it hides dependencies.
- IoC is the broad idea, DI is one kind of it, a container is a tool for DI.
- Aim for both: interface in the constructor, real classes wired once at startup.
Dependency inversion and dependency injection answer different questions. Inversion is the SOLID principle about types: business code depends on an abstraction it owns, not on a vendor's concrete class. Injection is a technique about creation: a class receives its dependencies, usually in the constructor, instead of building them. You can have either without the other. Injecting a concrete Razorpay class is injection without inversion, and in TypeScript a class with private fields will not even accept a lookalike fake. Fetching an interface from a global registry is a service locator, inversion without injection, and it hides dependencies until runtime. Inversion of control is the broader idea that a framework calls your code, and a DI container like the ones in NestJS or Angular is a tool that automates the wiring. I aim for both: ask for an interface in the constructor, and create the real objects in one composition root at startup.