Dependency Injection Explained

BY TOOLS.FUN  ·  MARCH 28, 2026  ·  6 min read

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.

Key point: Constructor injection is preferred over setter injection because it makes dependencies explicit and ensures the object is fully initialised before use. If a dependency is required, it should be in the constructor.

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.

Key point: DI without interfaces is just passing objects around. The real power comes from depending on abstractions so you can swap implementations without changing consumers.

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.

Key point: Start with manual DI (passing dependencies explicitly). Introduce a container only when the wiring becomes unwieldy — typically in larger applications with dozens of services.
← Back