Dependency Injection Explained
Dependency Injection (DI) is a design pattern where an object receives its dependencies from the outside rather than creating them itself. It is a specific form of Inversion of Control (IoC) that makes code more testable, flexible, and easier to maintain.
The Problem DI Solves
Without DI, a class creates its own dependencies:
class OrderService {
constructor() {
this.db = new PostgresDatabase();
this.mailer = new SmtpMailer();
}
}
This class is tightly coupled to Postgres and SMTP. You cannot test it without a real database, and swapping to a different mailer requires changing the class itself.
Constructor Injection
The most common DI approach passes dependencies through the constructor:
class OrderService {
constructor(db, mailer) {
this.db = db;
this.mailer = mailer;
}
}
Now the caller decides which implementations to provide. In tests, you pass mocks; in production, you pass real instances. The class itself does not care. You can validate your configuration objects with the JSON Formatter when using JSON-based DI configuration.
Inversion of Control
IoC is the broader principle: instead of your code controlling the flow and creating dependencies, the framework or container controls it. DI is one way to implement IoC. Event-driven systems, template methods, and strategy patterns are others. The key idea is that high-level modules should not depend on low-level modules — both should depend on abstractions.
DI Containers
A DI container (or IoC container) is a framework that automatically resolves and injects dependencies. You register types and their implementations, and the container wires them together:
# Python example with a simple container
container.register(Database, PostgresDatabase)
container.register(Mailer, SmtpMailer)
order_service = container.resolve(OrderService)
Popular containers include Spring (Java), .NET's built-in DI, and InversifyJS (TypeScript). For lightweight projects, manual injection (passing dependencies explicitly) works fine.
Interface Segregation
DI works best with interfaces (or abstract classes). Define a Database interface with methods like query() and execute(), then implement it for Postgres, MySQL, or an in-memory store. The consuming class depends on the interface, not the implementation. This is the "D" in SOLID — the Dependency Inversion Principle.
Testing with DI
DI makes unit testing straightforward. Instead of mocking global state or monkey-patching modules, you inject test doubles directly:
const mockDb = { query: jest.fn().mockResolvedValue([]) };
const mockMailer = { send: jest.fn() };
const service = new OrderService(mockDb, mockMailer);
Tests are fast, isolated, and deterministic. Use the Code Diff tool to compare test and production wiring configurations side by side.
Common Pitfalls
Over-injection: not every dependency needs DI — simple value objects and utility functions do not. Service locator anti-pattern: passing the entire container as a dependency hides what a class actually needs. Circular dependencies: if A depends on B and B depends on A, your design has a problem — DI containers may mask it temporarily. Keep your dependency graphs as a tree, not a web. Visualise configuration structures with the JSON to YAML Converter to keep DI config readable.