Detail chart · Backend

Dependency Injection

Pattern ◆◆◇◇◇

Provide dependencies to a component from the outside rather than constructing them internally.

Summary

Dependency Injection (DI) is a technique where a component receives its dependencies as constructor arguments, method parameters, or via a container — rather than instantiating them itself. It is the practical implementation of the Dependency Inversion Principle and a core enabler of testable, loosely coupled code.

Problem

When a class creates its own dependencies (new DatabaseConnection()), it becomes tightly coupled to that specific implementation. Changing the implementation, testing in isolation, or composing behavior becomes difficult.

Solution

Declare dependencies as constructor parameters typed to interfaces, not concrete classes. The caller — or a DI container — is responsible for constructing and passing the dependencies.

// Without DI — tightly coupled
class OrderService {
  private db = new PostgresDatabase(); // concrete, not swappable
}

// With DI — depends on abstraction
class OrderService {
  constructor(private db: Database) {} // interface, injectable
}

// Wired externally
const service = new OrderService(new PostgresDatabase());
// or in tests:
const service = new OrderService(new InMemoryDatabase());

DI containers (e.g., tsyringe, Spring, .NET’s built-in DI) automate the wiring based on type metadata or configuration.

The same shape in Python — dependencies typed to an abstract base and passed through the constructor:

from abc import ABC, abstractmethod

class Database(ABC):
    @abstractmethod
    def query(self, sql: str) -> str: ...

# Without DI — tightly coupled
class OrderService:
    def __init__(self):
        self.db = PostgresDatabase()  # concrete, not swappable

# With DI — depends on abstraction
class PostgresDatabase(Database):
    def query(self, sql: str) -> str:
        return f"[Postgres] {sql}"

class InMemoryDatabase(Database):
    def query(self, sql: str) -> str:
        return f"[InMemory] {sql}"

class OrderService:
    def __init__(self, db: Database):
        self.db = db  # interface, injectable

# Wired externally
service = OrderService(PostgresDatabase())
# or in tests:
service = OrderService(InMemoryDatabase())

Frameworks like dependency-injector or FastAPI’s Depends automate this wiring, but the constructor-injection shape works with no framework at all.

When to Use

Avoid when:

Trade-offs

BenefitCost
Classes are unit-testable with mock dependenciesWiring configuration adds indirection
Dependencies are explicit and visible at the call siteDI containers have learning curves and can hide complexity
Implementations are swappable without changing consumersOver-abstraction risk: not every collaborator needs an interface
Enforces Dependency Inversion Principle by defaultMagic DI containers can make debugging harder

Live Playground

Experiment with the pattern below. The two tabs show a tightly-coupled service (before.ts) and the DI-refactored equivalent (after.ts). Edit either file and the preview updates in real time.