Back to all terms
ContainerSvc ASvc BSvc C
State & Archintermediate

Dependency Injection

A design pattern where objects receive their dependencies from an external source rather than creating them internally, enabling loose coupling and testability.

Also known as: DI, IoC Container, Inversion of Control

Description

Dependency Injection (DI) is a technique where an object's dependencies are provided (injected) by an external entity rather than being created by the object itself. Instead of a UserService internally instantiating new PostgresUserRepository(), the repository is passed in through the constructor, a method parameter, or set by a framework. This inverts the control of dependency creation from the dependent object to an external assembler, which is why DI is a form of Inversion of Control (IoC).

DI comes in three flavors: constructor injection (dependencies passed via constructor parameters—the most common and recommended form), setter injection (dependencies set via methods after construction), and interface injection (the dependency provides an injector method that the client must implement). DI containers (IoC containers) automate dependency resolution: you register interfaces and their implementations, and the container constructs objects with all dependencies satisfied. Popular containers include tsyringe and inversify for TypeScript, Spring's ApplicationContext for Java, and .NET's built-in IServiceCollection. Angular has a built-in hierarchical DI system using @Injectable decorators and providers.

The primary benefit of DI is testability: when dependencies are injected, they can be replaced with mocks, stubs, or fakes during testing. A UserService that receives a UserRepository interface can be tested with an in-memory fake without touching a database. DI also enables the Open-Closed Principle: you can change behavior by injecting a different implementation without modifying existing code. The risk is over-engineering: not every dependency needs to be injected behind an interface. Focus DI on boundaries (database access, external APIs, time, randomness) rather than injecting every internal utility.

Prompt Snippet

Configure dependency injection using tsyringe with decorator-based registration: mark service classes with @injectable() and repositories with @singleton() lifecycle. Define a composition root in src/container.ts that registers all interface-to-implementation mappings using container.register<IUserRepository>(TOKENS.UserRepository, { useClass: PrismaUserRepository }). Use injection tokens as TypeScript symbols to avoid string-based magic keys. For testing, call container.register with mock implementations using { useValue: mockRepo } before each test. In the Express/Fastify route layer, resolve services from the container per-request to ensure proper scoping, and configure a request-scoped container for services that hold per-request state like the current authenticated user.

Tags

design-patternsarchitecturetestabilitysolid-principlesinversion-of-control