Dependency Injection
Supply a component's dependencies from the outside rather than letting it construct them — so implementations can be swapped without changing the component.
★★★★★5/5Inside a codebase — classes, modules, files
How it works
Dependency Injection (DI) is the practice of passing a component's collaborators to it (via constructor, method, or property) rather than letting it instantiate them with new or static calls. Martin Fowler named the pattern in 2004, building on the earlier Inversion of Control (IoC) principle.
In constructor injection — the preferred form — all required dependencies are declared in the constructor signature. A caller (or a DI container) provides them. The component only knows about the interface, not the concrete class.
DI containers (Spring, Angular's injector, NestJS, ASP.NET DI) automate the wiring: you register bindings (interface → class) once, and the container resolves the full dependency graph on demand. Go and Rust typically use manual constructor injection — simpler and just as effective.
Project structure
recommended layoutsrc/├── interfaces/│ ├── email-service.ts # Contract│ └── payment-service.ts├── services/│ ├── smtp-email.service.ts # Production implementation│ ├── mock-email.service.ts # Test / dev implementation│ └── order-service.ts # Depends on interfaces├── container.ts # Wire all dependencies once└── app.ts # Entry point — use container
Implementation
TypeScript · Go · Rust// interfaces/email-service.ts
export interface EmailService {
send(to: string, subject: string, body: string): Promise<void>;
}
// services/smtp-email.service.ts — production
export class SmtpEmailService implements EmailService {
constructor(private config: SmtpConfig) {}
async send(to: string, subject: string, body: string) {
await nodemailer.createTransport(this.config)
.sendMail({ to, subject, html: body });
}
}
// services/mock-email.service.ts — testing
export class MockEmailService implements EmailService {
readonly sent: Array<{ to: string; subject: string }> = [];
async send(to: string, subject: string) {
this.sent.push({ to, subject }); // no network, inspect in tests
}
}
// services/order-service.ts — receives dependencies, doesn't create them
export class OrderService {
constructor(
private repo: OrderRepository,
private email: EmailService, // ← interface, not SmtpEmailService
) {}
async placeOrder(order: NewOrder): Promise<string> {
const saved = await this.repo.save(order);
await this.email.send(
order.customerEmail, "Order confirmed", `Order ${saved.id} placed.`
);
return saved.id;
}
}
// container.ts — single place that knows about concrete classes
const email = new SmtpEmailService(config.smtp);
const repo = new PgOrderRepository(db);
export const orderService = new OrderService(repo, email);Why it matters
Without DI, components are tightly coupled to their collaborators. Changing an email provider, database, or third-party API requires modifying the component. With DI, you swap the implementation without touching the component — and unit tests inject mocks for complete isolation.
✓ When to use
- →Any class with external collaborators (databases, email, APIs, services)
- →When unit testing requires replacing real collaborators with test doubles
- →Medium-to-large codebases where wiring is too complex to manage manually
- →Frameworks like Spring, NestJS, Angular already expect it
✗ When NOT to use
- →Simple scripts or small utilities with no meaningful collaborators
- →When DI container overhead is a concern (some embedded or real-time systems)
Trade-offs
Collaborators are swappable — easy testing with mocks
DI containers add indirection that can make code harder to trace
Explicit dependencies improve readability and design
Constructor bloat when a class has many dependencies (signals it needs splitting)
Loose coupling enables parallel development and modularity
Misconfigured bindings fail at runtime (compile-time DI frameworks mitigate this)
In production
Entire Java/Kotlin enterprise ecosystem built on DI — @Autowired, @Bean, ApplicationContext
Hierarchical DI injector is core to how Angular components and services are composed
TypeScript DI container modelled after Angular; all providers are injected via decorators
Industry adoption
Related principles
Repository Pattern
Decouple business logic from data access by defining a collection-like interface for retrieving and persisting domain objects.
Hexagonal Architecture
Place the domain at the centre and connect all I/O (HTTP, database, messaging) through explicit ports and adapters — so the core is framework-free and infinitely testable.
Clean Architecture
LiveOrganise code into concentric dependency rings so business logic never depends on frameworks, databases, or UI.
Strategy Pattern
Define a family of interchangeable algorithms behind a common interface so the calling context can swap them at runtime without changing its own code.