Dependency Injection
Implement dependency injection to write testable, decoupled code across languages and frameworks.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Overview
Dependency Injection (DI) is a design pattern where objects receive their dependencies from external sources rather than creating them internally. It decouples components, makes code testable without mocks, and enables flexible composition of services.
When to Use
Use this resource when:
- Writing unit tests that require substituting real services with test doubles
- Building modular applications where components should not know about concrete implementations
- Managing complex object graphs with transitive dependencies
- Implementing plugin architectures or strategy patterns
Solution
Constructor Injection (TypeScript)
interface EmailService {
send(to: string, subject: string, body: string): Promise<void>;
}
class UserService {
constructor(
private emailService: EmailService,
private userRepository: UserRepository
) {}
async register(email: string, password: string) {
const user = await this.userRepository.create({ email, password });
await this.emailService.send(email, 'Welcome', 'Thanks for signing up!');
return user;
}
}
// Production wiring
const userService = new UserService(
new SendGridEmailService(),
new PostgresUserRepository()
);
// Test wiring
const userServiceTest = new UserService(
new FakeEmailService(),
new InMemoryUserRepository()
);
Property Injection (Python)
from typing import Protocol
class Logger(Protocol):
def log(self, message: str) -> None: ...
class ConsoleLogger:
def log(self, message: str) -> None:
print(f"[LOG] {message}")
class OrderProcessor:
logger: Logger = ConsoleLogger() # Default
def process(self, order: dict) -> None:
self.logger.log(f"Processing order {order['id']}")
# ...
DI Container (Java with Spring)
@Service
public class PaymentService {
private final PaymentGateway gateway;
private final FraudChecker fraudChecker;
public PaymentService(PaymentGateway gateway, FraudChecker fraudChecker) {
this.gateway = gateway;
this.fraudChecker = fraudChecker;
}
}
@Configuration
public class AppConfig {
@Bean
public PaymentGateway paymentGateway() {
return new StripeGateway();
}
}
Explanation
DI inverts control: instead of components finding or creating their dependencies, the container or caller provides them. This enables:
- Testability: Swap real services for fakes or stubs without modifying code
- Flexibility: Change implementations without touching consumers
- Lifecycle management: Containers can manage singletons, scoped instances, and disposal
- AOP support: Decorators and interceptors can be injected transparently
Variants
| Approach | Use Case | Trade-off |
|---|---|---|
| Constructor | Mandatory dependencies | Most explicit; best for testing |
| Property/Setter | Optional dependencies | Can create partially initialized objects |
| Method | Per-call dependencies | Verbose; used for strategy injection |
| Service Locator | Legacy code | Hides dependencies; harder to test |
Best Practices
- Prefer constructor injection: Makes dependencies explicit and immutable
- Avoid service locators: They hide dependencies and make testing harder
- Use interfaces/protocols: Depend on abstractions, not concrete types
- Keep composition roots shallow: Wire dependencies at the application entry point
- Avoid primitive obsession: Wrap config values in value objects (e.g., ApiKey, Timeout)
Common Mistakes
- Constructor explosion: More than 5 parameters signals a missing abstraction
- Leaking container: Passing the DI container into services defeats the purpose
- Tight coupling to framework: Use standard annotations (@Inject) when possible
- Ignoring lifecycle: Scoped services resolved as singletons cause memory leaks
- Circular dependencies: Refactor into events or a mediator if A depends on B and B on A
Frequently Asked Questions
Q: Is DI only for object-oriented languages? A: No. Functional languages achieve similar decoupling via higher-order functions and partial application.
Q: When should I use a DI container vs. manual wiring? A: Manual wiring for simple apps (<50 services). Containers for complex graphs, lifecycle management, or AOP.
Q: Does DI hurt performance? A: Negligible overhead at runtime. Resolve dependencies at startup (composition root), not per-request.
Related Resources
MVC Pattern
Separate application into Model, View, and Controller components. An architectural design pattern for organized, maintainable code.
PatternRepository Pattern
Abstract data access logic behind a clean interface. An architectural design pattern for testable, maintainable data layers.
PatternDependency Injection Pattern
Supply dependencies from outside rather than creating them internally. An architectural pattern for decoupled, testable code.
DocADR Template
A reusable template for Architecture Decision Records that capture context, decision, and consequences.
DocDatabase Schema Documentation Template
A template for documenting database schemas with entity relationships, field definitions, and migration history.