Dependency Injection Pattern
Supply dependencies from outside rather than creating them internally. An architectural pattern for decoupled, testable code.
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.
Dependency Injection Pattern
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
Frequently Asked Questions
Q: Is DI the same as Inversion of Control? A: 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.
Q: Do I need a DI framework? A: 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.
Q: How does DI help with testing? A: 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.
Is this pattern suitable for small projects?
For small projects with few components, this pattern may add unnecessary complexity. Start simple and introduce the pattern when you feel the pain it solves.
How does this pattern compare to alternatives?
Each pattern makes different trade-offs. Review the variants table above and consider your specific constraints: team size, performance requirements, and future scaling plans.
Can I partially apply this pattern?
Yes. Many teams adopt patterns incrementally. Start with the core idea and add sophistication as needed. The pattern is a guide, not a strict blueprint.
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. 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.
PatternContext Object Pattern
Encapsulate state and services needed by multiple components into a single context object, reducing method signature bloat and decoupling code from specific environment details.
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.
PatternNull Object Pattern
Use a default object instead of null references to eliminate null checks and simplify client code. A behavioral pattern for safer defaults.
PatternRegistry Pattern
Centralize access to shared services and objects via a lookup table. A structural pattern that decouples consumers from concrete implementations.