2026-04-22
Dependency Injection (DI) is deceptively simple: instead of a class creating its own dependencies, you pass them in from the outside. Yet this single idea transforms testability, flexibility, and the overall structure of your code.
The Problem. Consider an order service that directly instantiates a payment gateway:
class OrderService {
private gateway = new StripeGateway();
process(order) { this.gateway.charge(order.total); }
}
This code has three issues: you can't unit test OrderService without hitting Stripe's API, you can't swap to a different payment provider without editing this class, and you've violated the Dependency Inversion Principle — a high-level policy module depends on a low-level implementation detail.
The Fix. Inject the dependency through the constructor:
class OrderService {
constructor(private gateway: PaymentGateway) {}
process(order) { this.gateway.charge(order.total); }
}
Now in production you pass new StripeGateway(), in tests you pass a FakeGateway that records calls without network requests, and switching to a new provider means wiring up a different implementation — zero changes to OrderService.
Three styles of injection:
Do you need a DI container? Not always. For small-to-medium projects, manual "poor man's DI" — wiring dependencies at the composition root (your main() or app bootstrap) — is perfectly fine. Containers like InversifyJS, Spring, or .NET's built-in DI shine when you have dozens of services with deep dependency graphs. A practical rule of thumb: if you're manually wiring more than 15-20 dependencies in your composition root, consider a container.
Real-world example. A notification service needs to send alerts via email, SMS, or Slack depending on the customer's preference. Without DI, you end up with a switch statement and imports for every channel. With DI, you inject a NotificationChannel interface. Your factory or container resolves the right implementation based on config. Adding a new channel (say, Microsoft Teams) means writing one new class and one line of registration — the notification service itself never changes.
Common mistake: injecting too many dependencies. If a constructor takes more than 4-5 parameters, the class is likely doing too much. DI makes this smell visible — treat it as a signal to decompose the class, not as a reason to avoid injection.
