The idea
What it is
A dependency is just another object your object needs to do its job. A Car needs an Engine. An OrderService needs a Database. The question this lesson answers is small but it changes everything: who builds that dependency? If the object builds its own — new Engine() inside the Car — it is in control. If something outside builds it and hands it in, control has moved out. That second arrangement is what Dependency Injection and Inversion of Control are about.
Dependency Injection (DI) is the technique: instead of an object creating its own dependencies, you inject them from outside — almost always through the constructor. Inversion of Control (IoC) is the bigger idea behind it: your code stops being the thing that constructs and calls everything top-down. An outer assembler or framework now creates the pieces and wires them together. DI is simply the most common way to invert control over construction.
Picture a coffee machine. The cheap kind has a sealed-in water tank — to change the water you'd have to crack the machine open. The good kind has an opening on top that you pour water into. Same machine, but now you decide what goes in: filtered, sparkling, decaf. You change the input without rebuilding the machine. DI is that opening on top. The machine no longer fetches its own water; it accepts whatever you pour in.
The one sentence to remember
Don't let an object build its own dependencies — hand them in from outside. The object's job is to use its tools, not to make them.
The Hollywood Principle
IoC is often summed up as the Hollywood Principle: “Don't call us, we'll call you.” In ordinary code, your code is in charge — it creates objects and calls into libraries. Under IoC, you register your pieces and an outer assembler or framework calls them at the right time, with the right collaborators already plugged in. Control flips from your code to the framework.
Mechanics
How it works
The smell: a class that `new`s its own dependency
Here is the trap, and almost everyone writes it first. A class builds the concrete thing it needs inside itself:
class Car {
private engine = new PetrolEngine(); // ⚠️ car builds its own engine
private wheels = new Wheels(); // ⚠️ ...and its own wheels
drive() {
this.engine.start();
}
}Those new lines look harmless, but the Car has just taken on a second job it never wanted: manufacturing its own parts. That quietly causes two real problems. You can't swap a part — the day you want an ElectricEngine, you have to open Car and edit it, because the petrol engine is hard-wired inside. And you can't test it in isolation — every test of Car now spins up a real engine, because the car insists on building the real one. The car is welded to specific parts it should never have cared about.
The fix: inject the dependency through the constructor
Stop building parts inside the car. Declare what the car needs as parameters and let them be passed in. This is constructor injection — the most common, most honest form of DI:
class Car {
constructor(private engine: Engine, private wheels: Wheels) {} // handed in
drive() {
this.engine.start();
}
}
// somewhere at the edge of the app, the assembler builds and wires:
const car = new Car(new ElectricEngine(), new Wheels());Notice what just happened. The Car no longer says new anywhere. It receives an Engine and uses it. Whoever creates the car decides which engine goes in. Want electric? Pass an ElectricEngine. Want a fake for a test? Pass a FakeEngine. The Car class is never reopened. As a bonus, the constructor now lists every dependency up front — you can see exactly what a Car needs just by reading its signature.
The inversion: who is in control of building the parts?
Look at the direction of control. Before, the Car reached down and built its parts — control lived inside the car, and the build arrows pointed down and outward from it. After, an external assembler (often just your main()) builds the parts and passes them up into the car's constructor. Construction control moved from the object to the outside. That reversal — the object no longer controls how it is assembled — is the Inversion of Control. DI is the mechanism that carries it out.
Car reaches down and news its own Engine and Wheels — control over construction lives inside the car. Injected (right): an external Assembler / main() builds the parts and passes them up into the car's constructor — the arrows point into the Car, and control over construction has been inverted out of the object. Same parts get built; who builds them flipped.The composition root: where the wiring lives
If objects no longer build their own dependencies, something must. That place is the composition root — a single spot near the entry point of your app (main(), a startup file) where you construct the concrete pieces and wire the whole object graph together. Keeping the wiring in one place at the edge means the rest of your code stays free of new and free of construction concerns.
A DI container is optional
You do not need a framework to do DI. The simplest form is sometimes called “poor man's DI”: you just write the news by hand in main(). A DI container (Spring, Guice, .NET's built-in container) automates this for large graphs — you declare what implements what, and the container constructs and injects everything for you. That automated wiring is a form of IoC. But for a small app, hand-wiring at main() is perfectly good DI, and easier to follow.
How DI, IoC, and DIP relate
- IoC (Inversion of Control) is the broad principle: an outer assembler or framework controls construction and flow, not your object. “Don't call us, we'll call you.” DI is one kind of IoC (the kind about construction).
- DI (Dependency Injection) is the technique: pass dependencies in from outside instead of building them with
new. It's the most common way to achieve IoC over object construction. - DIP (Dependency Inversion Principle) is a separate, related idea — the D in SOLID — about which direction dependencies should point: toward abstractions (interfaces), not concrete classes. DI is how you usually deliver a DIP-friendly design: you inject an interface, and the assembler chooses the concrete implementation. You can do DI without DIP (inject a concrete type) and aim at DIP without a container — but in practice they travel together.
Interactive prototype
See it. Build it. Break it.
A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.
About this simulation
Two modes of the same Car, which needs an Engine and Wheels. In Self-service, the Car box literally contains new Engine() and new Wheels() — arrows point down as the car reaches in and builds its own parts, and the side panel shows swap a part? ✗ and test with a fake? ✗. Flip to Injected and an Assembler / main() box appears above the car; it builds the parts and passes them into the constructor, so the arrows now point up into the Car. A tray lets you hand in PetrolEngine, ElectricEngine, or FakeEngine (test) — each pick updates the car without changing the Car class, flips both indicators to green ✓, and the fixed panel narrates how construction control moved out of the car. One panel, replaced each click; nothing scrolls away.
Hands-on
Try these yourself
Open the prototype above, predict what happens, then verify.
Feel the self-service car
Open the prototype in Self-service mode. The Car box literally contains new Engine() and new Wheels(), and the build arrows point down as the car reaches in and constructs its own parts. Read the side panel: swap a part? ✗, test with a fake? ✗. The tray of other engines is disabled — there's no opening to hand anything in, because the car builds its own. This is the smell DI fixes.
Invert who builds the parts
Flip to Injected mode. Watch an Assembler / main() box appear above the car. It builds the Engine and Wheels and passes them up into the car's constructor — the arrows now point into the Car. Both indicators flip to green: swap? ✓, test? ✓. Nothing inside Car changed; construction control just moved out of it and onto the assembler. Read the line about the Hollywood Principle.
Hand in different parts without touching the Car
In Injected mode, use the tray to hand in an ElectricEngine — the explain panel confirms the Car class never changed. Now hand in a FakeEngine (test) and read why that makes the car testable: the fake just records that start() was called, so a unit test runs with no real engine. Click between the three engines and confirm the Car box stays identical every time — that is the payoff of injecting instead of building.
In practice
When to use it — and what trips people up
Inject the dependencies that vary or talk to the outside world
DI earns its keep around volatile or external dependencies — anything that touches the outside world, or anything you might reasonably want to replace, fake, or reconfigure. These are exactly the things that make code rigid and hard to test when you new them inline.
- External services — databases, payment gateways, email/SMS providers, an HTTP client to another team's API. Inject them so you can stub them in tests and swap providers in production.
- The clock and randomness — inject a
Clockand aRandomSourceso tests can pin time and seeds instead of being flaky. - Things you want to fake in a unit test — if you can't test a class without standing up real infrastructure, that dependency wants injecting.
- Configuration & policy choices — inject the strategy (which algorithm, which storage backend) so the choice is made once, at the composition root, not scattered through the code.
Don't inject everything
DI is a tool, not a tax on every line. Simple value objects (a Money, a Point), pure helpers, and standard-library types don't need injecting — they never vary and you'll never fake them. Forcing new-free purity on everything turns a tiny app into a maze of constructors and wiring with no payoff. Inject what is volatile or external; just new what is stable and trivial.
What it gives you
- Swappability — hand in a different implementation (petrol→electric, MySQL→Postgres) by changing one line at the composition root; the consuming class is never reopened.
- Testability — inject a fake or mock and unit-test the class with no database, network, or real engine in sight.
- Honesty — the constructor lists every dependency up front, so you can see exactly what a class needs just by reading its signature.
- Decoupling & boundaries — the object stops knowing how to build its collaborators, so each piece can change independently and teams can build them in parallel.
Common mistakes
- More wiring — something must construct and connect the graph at the app's edge; in a large app that composition root can sprawl.
- Indirection — to find what actually runs you may have to trace from the consumer back to the composition root, instead of reading it inline.
- Container complexity — a DI framework adds its own concepts (scopes, lifecycles, magic auto-wiring) and failures that surface at startup rather than at compile time.
- Over-application — injecting stable, trivial dependencies adds ceremony with no benefit and buries simple code under constructors.
Reference
Code & further reading
A minimal reference implementation and pointers worth bookmarking.
// BEFORE — Car builds (new's) its own engine. It's in control.
class CarV0 {
private engine = new PetrolEngine(); // ⚠️ can't swap, can't test
drive() { this.engine.start(); }
}
// AFTER — the engine is INJECTED through the constructor.
interface Engine { start(): void; }
class Car {
constructor(private engine: Engine) {} // handed in from outside
drive() { this.engine.start(); }
}
class PetrolEngine implements Engine {
start() { /* ...real petrol engine... */ }
}
class ElectricEngine implements Engine {
start() { /* ...real electric engine... */ }
}
// COMPOSITION ROOT — the assembler (main) builds & wires at the app edge.
// "Poor man's DI": just new things up by hand. No container needed.
function main() {
const car = new Car(new ElectricEngine()); // swap engine in ONE line
car.drive();
}
// Inject a FAKE in a test — fast, offline, asserts on a recording.
class FakeEngine implements Engine {
started = false;
start() { this.started = true; }
}
const fake = new FakeEngine();
new Car(fake).drive();
// expect(fake.started).toBe(true); // no real engine spun upReferences & further reading
6 sources- Articlemartinfowler.com
Inversion of Control Containers and the Dependency Injection pattern — Martin Fowler
The definitive essay. Fowler untangles Inversion of Control from Dependency Injection, names the three injection styles (constructor, setter, interface), and explains what a DI container actually does for you.
- Articlemartinfowler.com
Inversion of Control — Martin Fowler (bliki)
A short, sharp note on what IoC really means and why the term is broader than DI. This is where the “don't call us, we'll call you” framing is made precise.
- Docsen.wikipedia.org
Dependency injection — Wikipedia
A clear, language-agnostic overview: the four roles (service, client, interface, injector), constructor vs setter vs method injection, and how DI relates to IoC and the composition root.
- Docsdocs.spring.io
Spring Framework — The IoC Container (Core docs)
How a real-world DI container works: Spring constructs your beans and injects their collaborators for you. Good for seeing automated IoC at production scale.
- Docsgithub.com
Guice — Motivation & Getting Started
Google's lightweight DI container, opening with a before/after of hand-wiring vs injected code. A focused read on why a container helps once the object graph grows.
- Bookmanning.com
Dependency Injection Principles, Practices, and Patterns — Seemann & van Deursen
The canonical book on DI done right: the composition root, constructor injection as the default, anti-patterns to avoid, and when a container helps versus hurts. The deepest treatment if you want to go beyond the basics.
Knowledge check
Did it land?
Quick questions, answers revealed on submit. Sign in to save your best score.
question 01 / 05
What is Dependency Injection, in one sentence?
question 02 / 05
A Car contains private engine = new PetrolEngine(). Which two problems does this directly cause?
question 03 / 05
How do Inversion of Control (IoC), Dependency Injection (DI), and the Dependency Inversion Principle (DIP) relate?
question 04 / 05
Where does the wiring live in a DI-based app, and do you need a container?
question 05 / 05
Which dependency is the BEST candidate to just new inline rather than inject?
0/5 answered