Skip to main content

Dependency Injection

Dependency Injection supplies an object's collaborators from outside via constructor, setter, or interface, inverting control. It decouples classes and aids testing, with container indirection as the main cost.

Type
Creational
When to Use
Decouple Collaborators, Improve Testability, Swap Implementations, Centralize Wiring

Dependency Injection (DI) supplies an object's collaborators from outside rather than letting the object create or locate them itself. It is the most common expression of the Inversion of Control principle: instead of a class controlling how its dependencies are built, an external assembler controls that and hands them in. The result is loose coupling, easier testing, and configurable wiring.

How It Works

A class declares what it needs through abstractions (interfaces or types) and receives concrete instances from a caller. There are three injection styles. Constructor injection passes dependencies as constructor arguments — the preferred form, since it makes dependencies explicit and supports immutability and mandatory-dependency enforcement. Setter/property injection assigns them after construction, useful for optional dependencies. Interface/method injection passes them per call.

Wiring may be manual ("pure DI" — composing objects in a single composition root) or automated by a DI container (Spring, .NET's built-in container, Guice, Dagger, NestJS). A container reads registrations mapping abstractions to implementations and lifetimes (singleton, scoped, transient) and resolves entire object graphs on demand.

When to Use It

Use DI whenever a class collaborates with services that should be swappable, mocked in tests, or configured differently across environments. It is foundational in layered and microservice backends, where it cleanly separates business logic from infrastructure such as repositories, clients, and message buses.

For tiny scripts or objects with no meaningful dependencies, DI adds ceremony without benefit. Containers in particular can be overkill for small programs where manual wiring is clearer.

Trade-offs

DI decouples classes from concrete collaborators, makes dependencies visible, and lets tests inject fakes without touching production code. It centralizes configuration and supports varying implementations per environment.

The costs include indirection — following object construction across a container's registrations can be opaque — and a learning curve for container features and lifetimes. Misconfigured lifetimes (for example, injecting a scoped service into a singleton) cause subtle, hard-to-find bugs. Overuse can produce sprawling constructors; that is usually a signal a class has too many responsibilities. Prefer DI over the related Service Locator pattern, which hides dependencies behind a global lookup and reintroduces the coupling DI removes.

Related Patterns

DI is the modern alternative to Singleton for sharing a single instance, achieved via singleton-scoped registration. It is often combined with Abstract Factory when a dependency must be created on demand with runtime arguments. Service Locator is a related but generally discouraged sibling because it obscures dependencies.

Example

An OrderService needs a PaymentGateway and a Repository. Instead of new StripeGateway() inside the class, both are constructor parameters: OrderService(PaymentGateway gw, Repository repo). In production the container injects StripeGateway and SqlRepository; in unit tests the code injects an in-memory repository and a fake gateway, allowing fast, isolated tests of the order logic with no real network or database.