Dependency injection

On this page 7

Dependency injection is a technique in software architecture in which an object or function receives the components it depends on from the outside rather than constructing or locating them itself. The code that assembles the application decides which concrete implementations to supply, so the dependent code relies only on abstractions such as interfaces.

Dependency injection is the most common way to apply the dependency inversion principle, the D in SOLID, and a specific form of inversion of control. It makes code easier to test, replace and configure, and it underpins clean architecture styles where business logic must not depend on databases, frameworks or user interfaces.

How dependency injection works

Without dependency injection, a class such as OrderService might write new PostgresOrderRepository() inside its constructor. It is then hard-wired to one implementation: tests cannot replace the database, and switching storage means editing the class.

With dependency injection, OrderService declares that it needs an OrderRepository and receives one through its constructor. A separate place, called the composition root, creates the concrete repository and passes it in. Only the composition root knows about concrete classes; everything else depends on contracts.

The table below names the roles involved in every injection, whether it is done by hand or by a framework.

RoleResponsibilityExample
ClientUses a dependency through an abstractionOrderService
Service or dependencyProvides the needed capabilityPostgresOrderRepository
AbstractionDefines the contract between themOrderRepository interface
Injector or composition rootCreates objects and wires them togethermain function or DI container
Roles in dependency injection

Forms of dependency injection

Dependencies can be supplied in several ways, and the choice affects how explicit and safe the design is. Constructor injection is the recommended default in most modern guidance.

FormHow the dependency arrivesAdvantagesDrawbacks
Constructor injectionConstructor parametersRequired, visible, allows immutable fieldsLong constructors reveal too many dependencies
Setter or property injectionSetter method after constructionSuits optional dependenciesObject may be used before it is complete
Method injectionParameter of a single method callDependency varies per callCallers must supply it every time
Interface injectionMethod defined by an injector interfaceExplicit injection contractRarely used, adds boilerplate
Injection forms and their trade-offs

In functional programming, the same idea appears as passing functions as parameters, partial application or a reader pattern. The mechanism differs, but the principle of supplying collaborators from outside is identical.

Manual wiring and DI containers

Dependency injection does not require a framework. Manual wiring, sometimes called pure DI, builds the object graph with plain constructor calls in the composition root. It is fully type-checked by the compiler and works well for small and medium applications.

As graphs grow to hundreds of classes, teams often use a DI container, which registers types and constructs graphs automatically. Containers also manage object lifetimes, which is one of their most valuable features.

LifetimeInstance behaviorTypical use
TransientNew instance for every requestLightweight, stateless helpers
ScopedOne instance per scope, such as an HTTP requestDatabase contexts, unit of work
SingletonOne instance for the whole applicationConfiguration, connection pools, caches
Common object lifetimes in DI containers

Well-known containers include Spring in Java, the built-in container of ASP.NET Core, Dagger for Android and NestJS for TypeScript. Dagger resolves the graph at compile time, while most others use runtime reflection, which trades startup speed and early error detection for flexibility.

Dependency injection is often mixed up with other design patterns that also decouple object creation. The comparison below shows how they differ.

PatternWho obtains the dependencyVisibility of dependencies
Dependency injectionAn external injector supplies itExplicit in constructor or parameters
Service locatorThe class asks a global registryHidden inside method bodies
FactoryThe class calls a factory to create itFactory is visible, product is abstract
Singleton accessThe class reads a global instanceHidden, creates global coupling
Dependency injection and neighboring patterns

The service locator is widely considered an anti-pattern in application code because it hides dependencies and causes failures at runtime rather than at construction. Factories remain useful together with injection when an object must be created on demand with runtime data.

Benefits, costs and common mistakes

The main benefit is testability: a unit test can pass a mock or an in-memory fake instead of a real database or payment gateway. Further benefits are replaceable implementations, clear dependency graphs and configuration that changes per environment without code changes.

  • Constructor over-injection: more than about five dependencies usually signals a class with too many responsibilities.
  • Captive dependencies: a singleton holding a scoped object keeps it alive too long and can leak data between requests.
  • Container everywhere: passing the container itself into classes turns injection into a service locator.
  • Abstractions for everything: interfaces with exactly one implementation and no testing need add indirection without value.
  • Magic configuration: heavy annotation scanning makes it hard to find where an implementation is chosen.

The guiding rule is to inject at boundaries that matter, such as storage, network, time and external services, and to keep the wiring in one visible composition root.

A non-technical view

Picture a chef who must grow vegetables and raise chickens before cooking. That chef is tied to one farm. A better kitchen has ingredients delivered, so the chef only cooks, and the supplier can change without changing the recipes.

Dependency injection does the same for software. Each part of a program states what it needs and receives it from outside. Swapping a real bank connection for a practice one during testing then becomes as easy as switching suppliers.

A concrete example

A subscription billing service needs a payment gateway, a clock and an email sender. Originally the billing class created all three itself, so its tests charged a sandbox payment account, waited for real dates and occasionally sent test emails to customers.

The team refactors the class to receive three interfaces through its constructor. In production, the composition root supplies the real gateway, the system clock and the email provider. In tests, it supplies a fake gateway, a fixed clock set to the last day of the month and an in-memory mailbox. The billing test suite now runs in under two seconds instead of several minutes, covers renewal edge cases that previously depended on the calendar, and no longer touches any external system.