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.
| Role | Responsibility | Example |
|---|---|---|
| Client | Uses a dependency through an abstraction | OrderService |
| Service or dependency | Provides the needed capability | PostgresOrderRepository |
| Abstraction | Defines the contract between them | OrderRepository interface |
| Injector or composition root | Creates objects and wires them together | main function or DI container |
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.
| Form | How the dependency arrives | Advantages | Drawbacks |
|---|---|---|---|
| Constructor injection | Constructor parameters | Required, visible, allows immutable fields | Long constructors reveal too many dependencies |
| Setter or property injection | Setter method after construction | Suits optional dependencies | Object may be used before it is complete |
| Method injection | Parameter of a single method call | Dependency varies per call | Callers must supply it every time |
| Interface injection | Method defined by an injector interface | Explicit injection contract | Rarely used, adds boilerplate |
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.
| Lifetime | Instance behavior | Typical use |
|---|---|---|
| Transient | New instance for every request | Lightweight, stateless helpers |
| Scoped | One instance per scope, such as an HTTP request | Database contexts, unit of work |
| Singleton | One instance for the whole application | Configuration, connection pools, caches |
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 compared with related patterns
Dependency injection is often mixed up with other design patterns that also decouple object creation. The comparison below shows how they differ.
| Pattern | Who obtains the dependency | Visibility of dependencies |
|---|---|---|
| Dependency injection | An external injector supplies it | Explicit in constructor or parameters |
| Service locator | The class asks a global registry | Hidden inside method bodies |
| Factory | The class calls a factory to create it | Factory is visible, product is abstract |
| Singleton access | The class reads a global instance | Hidden, creates global coupling |
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.