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))