Overview
With dependency injection (DI), a component receives its collaborators (database client, clock, mailer, config) through a constructor argument, function parameter, or framework wiring instead of building them in its own body. Introduce it when a unit test needs a fake database, clock, or HTTP client, or when wiring differs between environments.
Rules
- Start with plain parameters. Add a DI container (Spring, NestJS, FastAPI’s
Depends()) only when wiring becomes deep enough to hurt. - Inject the boundaries (network, clock, filesystem, randomness), not every helper.
- Skip DI for pure functions and small scripts, where the indirection costs more than it saves.
# Hard to test: constructs its own client.
def send_invoice(invoice):
SMTPClient(host="smtp.example.com").send(invoice.email, render(invoice))
# Injected: tests pass a fake Mailer.
def send_invoice(invoice, mailer: Mailer):
mailer.send(invoice.email, render(invoice))Related
- general-principles: the design principles behind DI.
- testing: DI makes unit tests cheap.
- fastapi-dependencies:
Depends()in FastAPI. - side-effect: DI makes side effects explicit and swappable.
- twelve-factor: config and backing services arrive from outside.