Dependency Injection Pattern
Supply dependencies from outside rather than creating them internally. An architectural pattern for decoupled, testable code.
Overview
The Dependency Injection Pattern is an architectural pattern where dependencies are supplied to a class from the outside rather than being created internally. This inverts control: the class declares what it needs, and an external mechanism provides it. The result is loosely coupled, highly testable code.
When to Use
Use Dependency Injection when:
- Classes depend on other classes and you want to avoid tight coupling
- You need to substitute implementations for testing (mocks, stubs)
- You want to configure behavior at runtime or deployment time
- You are building a plugin or modular architecture
- You want to follow the Dependency Inversion Principle (SOLID)
Solution
Python
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def charge(self, amount: float) -> bool:
pass
class StripeProcessor(PaymentProcessor):
def charge(self, amount: float) -> bool:
print(f"Charging ${amount} via Stripe")
return True
class PayPalProcessor(PaymentProcessor):
def charge(self, amount: float) -> bool:
print(f"Charging ${amount} via PayPal")
return True
class OrderService:
def __init__(self, processor: PaymentProcessor):
# Dependency injected via constructor
self.processor = processor
def checkout(self, amount: float) -> bool:
return self.processor.charge(amount)
# Usage: swap implementations easily
stripe_service = OrderService(StripeProcessor())
stripe_service.checkout(100.0)
# Testing: inject a mock
class MockProcessor(PaymentProcessor):
def charge(self, amount: float) -> bool:
return True
test_service = OrderService(MockProcessor())
assert test_service.checkout(1.0)
JavaScript
class StripeProcessor {
charge(amount) {
console.log(`Charging $${amount} via Stripe`);
return true;
}
}
class PayPalProcessor {
charge(amount) {
console.log(`Charging $${amount} via PayPal`);
return true;
}
}
class OrderService {
constructor(processor) {
this.processor = processor;
}
checkout(amount) {
return this.processor.charge(amount);
}
}
// Usage
const stripeService = new OrderService(new StripeProcessor());
stripeService.checkout(100.0);
// Testing with mock
class MockProcessor {
charge(amount) { return true; }
}
const testService = new OrderService(new MockProcessor());
console.assert(testService.checkout(1.0));
Java
public interface PaymentProcessor {
boolean charge(double amount);
}
public class StripeProcessor implements PaymentProcessor {
public boolean charge(double amount) {
System.out.println("Charging $" + amount + " via Stripe");
return true;
}
}
public class PayPalProcessor implements PaymentProcessor {
public boolean charge(double amount) {
System.out.println("Charging $" + amount + " via PayPal");
return true;
}
}
public class OrderService {
private final PaymentProcessor processor;
// Constructor injection
public OrderService(PaymentProcessor processor) {
this.processor = processor;
}
public boolean checkout(double amount) {
return processor.charge(amount);
}
}
// Usage
OrderService stripeService = new OrderService(new StripeProcessor());
stripeService.checkout(100.0);
Explanation
Dependency Injection has three common forms:
- Constructor Injection — dependencies passed via the constructor (most common, ensures the object is always fully initialized)
- Setter Injection — dependencies set via setter methods after construction (flexible, but object may be in incomplete state)
- Interface Injection — dependencies provided through an interface method (less common, used in frameworks)
The core idea is Inversion of Control: instead of a class creating its own dependencies, they are supplied externally.
Variants
| Variant | Description | Best For |
|---|---|---|
| Constructor Injection | Dependencies passed at creation | Mandatory dependencies; immutable services |
| Setter Injection | Dependencies set after creation | Optional dependencies; reconfiguration at runtime |
| Interface Injection | Dependencies via interface method | Framework-managed lifecycle |
| Service Locator | Class asks a registry for dependencies | Legacy systems; avoid in new code |
| DI Container | Framework resolves and injects dependencies automatically | Large applications (Spring, Angular, .NET Core) |
What Works
- Prefer constructor injection for required dependencies; it makes the class’s needs explicit
- Use interfaces or abstractions as dependency types, not concrete classes
- Avoid service locators when possible; they hide dependencies and make testing harder
- Keep DI configuration separate from business logic (use modules, config files, or annotations)
- Respect the Law of Demeter — don’t inject the container itself, only the specific dependencies needed
Common Mistakes
- Injecting the DI container itself instead of specific dependencies, creating a service locator anti-pattern
- Using setter injection for required dependencies, allowing objects to exist in an incomplete state
- Over-engineering with a DI container for small projects where manual wiring is simpler
- Allowing circular dependencies between injected services, causing initialization failures
- Forgetting to register all dependencies in the container, leading to runtime resolution errors
Troubleshooting
- Pattern does not fit the problem: re-evaluate the forces (performance, scalability, team size, coupling). A pattern is only appropriate when its trade-offs match your constraints.
- Too many abstractions: if adding a pattern increases complexity without a clear benefit, simplify. Not every module needs a factory, decorator, or strategy.
- Tight coupling after refactoring: check that interfaces are stable and dependencies point inward.
- Tests break when the design changes: favor stable contracts over internal structure.
- Performance regression from indirection: measure before and after. Layers, decorators, and adapters can add latency; cache or inline hot paths if needed.
Further Reading
- Official documentation: check the current reference for the framework or tool used.
- Related guides: explore the architecture-pattern and decoupling guides for deeper coverage.
- Complementary patterns: review design patterns applicable to your technology stack.
- Public postmortems: study real incidents from teams that faced similar production issues.
Production Notes
- Deploy gradually using canary or blue-green to catch regressions early.
- Configure alerts for error rate, p99 latency, and failure rate before enabling in production.
- Document the rollback in the runbook; test the procedure in staging at least once per quarter.
- Review structured logs with correlation IDs to trace requests end-to-end during incidents.
Key Takeaways
- Apply dependency injection pattern when you need a practical solution for your use case.
- Monitor performance after implementation; measure latency, errors, and resource usage before and after.
- Check the Troubleshooting section for common failures; most have documented root causes with fixes.
- Keep dependencies updated and run tests in CI to prevent production regressions.
Advanced Topics
Scenario: DI for Notification Service
// DI pattern: inject dependencies instead of creating them
// Without DI (coupled)
class BadNotificationService {
private emailProvider = new SendGridProvider(); // coupled
private logger = new ConsoleLogger(); // coupled
async notify(email: string, msg: string) {
await this.emailProvider.send(email, msg);
this.logger.log(`Sent to ${email}`);
}
}
// With DI (decoupled)
interface EmailProvider { send(to: string, body: string): Promise<void>; }
interface Logger { log(msg: string): void; }
class GoodNotificationService {
constructor(
private emailProvider: EmailProvider,
private logger: Logger
) {}
async notify(email: string, msg: string) {
await this.emailProvider.send(email, msg);
this.logger.log(`Sent to ${email}`);
}
}
// Composition: choose implementations at construction
const service = new GoodNotificationService(
new SendGridProvider(), // or new SESProvider(), or new MockProvider()
new WinstonLogger() // or new ConsoleLogger(), or new SilentLogger()
);
// In tests: inject mocks
const mockEmail: EmailProvider = { send: async (to, body) => { console.log(`Mock send to ${to}`); } };
const mockLogger: Logger = { log: (msg) => { /* spy */ } };
const testService = new GoodNotificationService(mockEmail, mockLogger);
// Injection types
| Type | Example | Advantages | Disadvantages |
|------|---------|------------|---------------|
| Constructor | constructor(db: DB) | Mandatory deps | Long params |
| Setter | setDB(db: DB) | Optional, flexible | Deps can be missing |
| Interface | @Injectable() | Metadata, DI container | Requires framework |
| Property | @Inject() | Concise | Hidden deps |
| Method | process(db: DB, data) | Per call | Repetitive |
Lessons:
- DI decouples: the service does not create its dependencies
- Constructor injection is preferred: mandatory and explicit deps
- In tests, inject mocks: do not touch real services
- DI container automates composition (tsyringe, InversifyJS)
- Without DI container: manual composition at entry point
- DI vs Service Locator: DI is explicit, SL is implicit and hidden
### DI vs Service Locator: which do I use?
Use DI: dependencies are passed to the constructor, visible and mandatory. Use Service Locator only in legacy: the service asks a global registry for dependencies. DI is explicit: you see what the service needs. SL is implicit: the service fetches dependencies internally, hiding coupling. DI is testable; SL is hard to test. Prefer DI always.
End of document. Review and update quarterly.
## Common Production Pitfalls
- Applying the pattern where no abstraction is needed, adding accidental complexity.
- Letting the pattern leak into unrelated modules and blur ownership boundaries.
- Over-engineering the first implementation instead of starting simple and measuring pain.
- Skipping contract tests, so refactors silently break consumers.
- Ignoring failure modes that the pattern does not cover.
- Using the pattern as a default instead of choosing the right tool for the current scale.
- Forgetting to document when to stop using the pattern and what replaces it.
- Missing observability around the pattern's performance and error propagation. Frequently Asked Questions
Is DI the same as Inversion of Control?
DI is a specific form of IoC. IoC is the broader principle of delegating control to external code. DI achieves IoC by injecting dependencies from outside.
Do I need a DI framework?
No. For small projects, manual constructor injection is sufficient. See DI Container in TypeScript for a lightweight implementation. DI frameworks like Spring, Angular's injector, or InversifyJS shine in large applications with many interdependent services.
How does DI help with testing?
By depending on abstractions (interfaces), you can inject mock or stub implementations during tests. See unit testing for testing patterns. This isolates the class under test from its real collaborators.
Related Resources
Factory Pattern
Create objects without specifying the exact class to instantiate. A creational design pattern for flexible object creation.
PatternSingleton Pattern
Ensure a class has only one instance and provide global access to it. A creational design pattern for controlled object creation.
PatternStrategy Pattern
Define a family of algorithms, encapsulate each one, and make them interchangeable. A behavioral design pattern for flexible behavior selection.
PatternRepository Pattern with TypeScript Generics
Implement a type-safe repository pattern in TypeScript that decouples data access logic from domain services using generics and interfaces.
PatternManager Pattern
Encapsulate lifecycle, coordination, and access control for a set of related objects through a dedicated manager class that centralizes operations and enforces invariants.
GuideClean Architecture
A practical guide to Uncle Bob's Clean Architecture: organize code into layers so that frameworks, UI, and databases are details, not dependencies.