Dependency Injection
A class receives what it needs instead of building it. Know the three forms Fowler named, why constructor injection is the default, what a Service Locator costs, and where the wiring lives.
Overview
Dependency Injection (DI) means an object is handed the things it depends on, instead of creating them itself. OrderService does not write self.repo = PostgresOrders(); it takes a repository as a constructor argument, and whoever builds the service decides which one. The class becomes a description of what it needs; the choice of what to give it moves out to one place.
Martin Fowler named the technique in January 2004. The lightweight containers of the time were being described as 'inversion of control', and Fowler argued that the term was too broad: frameworks invert control by nature, so saying a container is special for that reason is like saying a car is special because it has wheels. He and his colleagues chose 'Dependency Injection' to name the specific thing these containers did, and he described three forms: constructor, setter and interface injection.
Interviewers use DI to check whether you can explain the small mechanism (pass it in), the effect on tests (pass a fake in), the choice of form (constructor by default), and the difference from a Service Locator. Strong answers also know where the wiring goes: a composition root, whether written by hand or by a framework.
The mechanism
- The dependency — Something the class needs: a repository, a clock, a mailer
- The abstraction — An interface (or Python protocol) the class depends on
- The injection — Passing the concrete object in: constructor, setter or interface method
- The composition root — The one place that builds concrete objects and wires them together
The three forms
Fowler's article names three, each with the container that used it at the time: constructor injection (PicoContainer), setter injection (Spring), interface injection (Avalon).
- Constructor — Dependencies are constructor parameters — From the moment it exists — Required dependencies; the default
- Setter — A method per dependency, called after construction — Only after every setter is called — Optional dependencies with a sensible default
- Interface — The class implements an injection interface the container knows — After the container calls it — Rare; Fowler calls it more invasive because of the extra interfaces
Fowler's advice is to start with constructor injection and move to setters only when it becomes a problem, for example with many parameters. The Spring reference says the same in its own words: constructors for mandatory dependencies, setters for optional ones, and a large number of constructor arguments is a code smell that points at too many responsibilities.
A runnable example
The service takes its two dependencies in the constructor. Production wiring and test wiring differ only in what is passed.
A production build() would return OrderService(PostgresOrders(url), SystemClock()). The class under test is identical in both.
Where the wiring lives
Every injected object has to be built somewhere. If that 'somewhere' is spread across the code, DI has only moved the problem. The answer is one composition root: a main function, a small factory module, or a container that reads declarations and does the same job.
- How dependencies are found — You write the constructor calls — The container reads declarations (Spring annotations, for example) and matches types
- Cost — A few dozen lines that grow with the app — Learning the container and its lifecycle rules
- Failure mode — A compile or import error where a dependency is missing — A start-up error from the container
- In Fowler's terms — The assembler is code you write — The container is the assembler
Service Locator: the alternative and its cost
A Service Locator is a registry the class asks for its dependencies: repo = locator.get("orders"). Fowler compares the two at length and does not call the locator an anti-pattern; he says the key difference is that every user of a service now depends on the locator, and that for a reusable component, DI is the better choice because a locator is an assumption you force on every user.
- Dependencies visible — Inside method bodies — In the constructor signature
- Testing — The test registers a fake; Fowler notes this is easy if the locator is swappable, but a global registry can leak between tests — The test passes a fake; nothing is shared
- What the class depends on — Its real dependencies plus the locator — Its real dependencies
- Reuse in another application — Only if that application adopts the same locator — Any caller can construct it
What you will be able to do
- Explain DI as 'passed in, not built inside' and say why Fowler chose the name.
- Name the three forms and defend constructor injection as the default.
- Write a service with injected dependencies and test it with fakes.
- Describe a composition root and say why the wiring belongs in one place.
- Compare DI with a Service Locator without calling either an anti-pattern.
Interview questions
- What are the three forms of dependency injection, and which should be the default?
- How does dependency injection improve unit testability?
- What is Inversion of Control, and how does it relate to DI?
- Is Service Locator an anti-pattern? Compare it with DI.
- A class takes eight constructor parameters. What does that tell you, and what do you do?
Sign up free to read the full topic
More Low-Level Design topics
Start free on Cariara · Coding problems · System design studio