Dependency injection
Follow the history of IoC, DIP, and DI to distinguish object use, assembly, and lifetime, then test the relationship with a small sender example.
· Last updated: 2026-11-04 · 7 min read
Overview
Dependency injection (DI) configures objects or functions by supplying their collaborators from outside. Notification code uses a sender, while the code assembling the application chooses an email sender or a text-message sender. Separating use from configuration lets the application change delivery mechanisms while keeping the notification logic.
A dependency is something another piece of code needs to do its work. Database access objects, clocks, and file stores can all be dependencies. Injection describes a relationship: assembly code supplies a collaborator that the consumer would otherwise construct internally. An ordinary constructor argument or function parameter can establish that relationship.
Applications face different kinds of change. Revising a discount rule changes business logic; connecting that same rule to a test store changes configuration. Mixing the two can require a real database just to run a test, or a business-code change just to switch delivery mechanisms. DI makes the connection point explicit.
To understand a DI design, ask what will be used, who connects it, and how long the connected object lives. The first two questions concern configuration; the last concerns object lifetime.
History
1988: Ralph Johnson and Brian Foote described a reusable framework calling application-specific behavior as inversion of control (IoC) in “Designing Reusable Classes”.1
May 1996: Robert C. Martin presented the dependency inversion principle (DIP), concerning source dependency direction, in “The Dependency Inversion Principle” in C++ Report.2
January 23, 2004: Martin Fowler described the wiring used by lightweight Java containers such as PicoContainer and Spring as Dependency Injection and compared it with Service Locator in “Inversion of Control Containers and the Dependency Injection pattern”. The article records the naming and comparison following discussion with IoC advocates.3
The framework discussion of 1988 concerns control flow, DIP in 1996 concerns source dependency direction, and the 2004 DI article concerns object configuration. These discussions connect, but they ask different questions.
Inversion of control and frameworks
In 1988, Ralph E. Johnson and Brian Foote examined control flow in reusable frameworks. An application calls a library when it needs a function; a framework calls application-specific behavior within its reusable execution structure. Johnson and Foote called this inverted calling relationship inversion of control. Their concern was framework reuse: which behavior should change through inheritance, and which parts should be supplied as interchangeable objects while the execution structure stays in place?1
IoC in this sense is broader than a modern DI container: it also describes an event loop invoking callbacks. Supplying objects from outside is a more specific configuration problem within that wider discussion.
Dependency direction and DIP
Robert C. Martin's dependency inversion principle, presented in May 1996, concerns source dependency direction. High-level policy and low-level implementation both depend on abstractions, reducing the extent to which implementation changes require policy changes.2 Alongside control flow, this raises the question of whether policy must know the concrete type of a device or store.
DI asks which collaborator is supplied; DIP asks which types the receiving code depends on. A constructor can receive a concrete email sender, establishing external configuration while leaving a separate question about the dependency between policy and that concrete type.
Injection styles and Service Locator
Martin Fowler's article of January 23, 2004 examines how much configuration detail should remain in consumer code when components from different teams are assembled. He found IoC too broad a name to identify the particular inversion and, after discussion with IoC advocates, described this wiring under the more specific name Dependency Injection.3
Constructor injection supplies collaborators during construction, setter injection connects them afterward, and interface injection expresses the receiving operation as an interface. The distinction concerns where configuration and use occur, rather than one container's syntax.
Fowler also compared DI with Service Locator. With a locator, the consumer requests an object from a service registry; with DI, collaborators arrive as arguments. Both allow configuration changes, but dependencies are discovered in different places.3 Incoming contracts can be read in the consumer's declaration, while locator lookups require reading its body. Fowler's separation of configuration from use applies to manual assembly as well as container-based applications.
DI today: lifetimes and assembly
Containers also construct and share the objects they connect, so modern DI explanations discuss lifetimes such as transient, scoped, and singleton. In .NET's container, transient services are created whenever resolved, scoped services are shared within a scope, and singleton services are shared within the container. A web request commonly provides a scope boundary.4
Spring likewise defines singleton in terms of a container and bean definition, while prototype scope supplies a new instance when requested. A prototype dependency injected during singleton construction is resolved at that point, so the singleton retains the reference it received.5
Object lifetime adds a question beyond which implementation to connect: how long will the consumer retain its state? A long-lived object can retain request state, or the responsibility for closing a file or connection can be missed, leaving configuration at odds with execution lifetime.
Polymorphism lets a consumer use several implementations through a common calling contract. DI lets assembly code outside the consumer choose what to connect. Supplying a concrete type is still DI, even without polymorphism. A container performs this assembly, while an ordinary function call can expose the same relationship in a small program.
A small example
This example separates notification code from the code choosing a sender. Returning delivery results as strings makes the selected collaborator visible without a network.
type Sender = { send(message: string): string };
function createNotifier(sender: Sender) {
return {
notify(name: string) {
return sender.send(`Hello, ${name}`);
},
};
}
const mail: Sender = { send: message => `mail:${message}` };
const sms: Sender = { send: message => `sms:${message}` };
console.log(createNotifier(mail).notify('Ada'));
console.log(createNotifier(sms).notify('Ada'));
The output is mail:Hello, Ada and sms:Hello, Ada. Assembly code chooses mail or sms, passes it to createNotifier, and calls notify('Ada'); notify calls the supplied sender.send. The same notification logic runs with two different senders.
Footnotes
- Ralph E. Johnson and Brian Foote, “Designing Reusable Classes,” JOOP 1(2), 1988, pp.22–35, “White-box vs Black-box Frameworks.” Authors' copy. ↩
- Robert C. Martin, “The Dependency Inversion Principle,” C++ Report, May 1996 (Engineering Notebook column). Author's paper. ↩
- Martin Fowler, “Inversion of Control Containers and the Dependency Injection pattern,” 2004-01-23. Article. ↩
- Microsoft, “Service lifetimes.” Official documentation. ↩
- Spring Framework, “Bean Scopes.” Official documentation. ↩