Skip to content

Dependency Injection — Detailed#

Without DI vs with DI#

classDiagram
  direction LR
  class OrderService_Bad {
    +place()
  }
  class HttpClient
  OrderService_Bad ..> HttpClient : new HttpClient
  note for OrderService_Bad "Hidden dependency. Can't swap, can't mock."

  class OrderService_Good {
    -client: HttpClient
    +OrderService(HttpClient)
    +place()
  }
  OrderService_Good --> HttpClient : injected
  note for OrderService_Good "Explicit dependency.\nSwap real vs fake at the composition root."

Forms of injection#

Form Example When to use
Constructor injection new OrderService(repo, gateway) default; makes deps required & immutable
Setter injection svc.setLogger(l) optional collaborators
Interface injection callback svc.inject(this) rare; OSGi-style
Field injection @Autowired Repo repo; framework-only; harder to test

Prefer constructor injection.

Containers#

Frameworks that do the wiring for you by reading metadata (annotations / XML / code):

  • Java: Spring, Guice, Dagger
  • C#: built-in Microsoft.Extensions.DependencyInjection
  • Python: dependency-injector, FastAPI's Depends
  • Go: explicit, no container (functional options preferred)

Containers solve the "object graph wiring" problem but introduce magic and a learning curve. In small apps, hand-wired DI in main() is often clearer.

Lifecycle scopes#

Scope What it means
Singleton one instance per container (default for stateless services)
Request / per-call new instance per HTTP request — for stateful per-request work
Transient new instance every time it's resolved
Scoped matches a custom scope (e.g. background job)

Composition root#

The one place that knows about every concrete class in your app. Keep it small. Everything else only knows abstractions.

public static void main(String[] args) {
    var clock      = new SystemClock();
    var repo       = new PostgresOrderRepo(dataSource);
    var gateway    = new StripeGateway(httpClient, apiKey);
    var notifier   = new EmailNotifier(smtpHost);
    var service    = new OrderService(repo, gateway, notifier, clock);
    new HttpServer(service).start();
}

Anti-patterns#

  • Service Locator — code asks a global for its deps (Locator.get(Foo.class)). Looks like DI, smells like singletons. Prefer constructor injection.
  • Newing inside constructors — defeats DI: Foo() { this.bar = new Bar(); }.
  • Auto-wiring everything — opaque object graphs. Frameworks should help, not hide.
  • Container in the domain layer — only the composition root (or a thin facade) should reference the container.

Where DI shows up in this site#

Practically every microservice has a composition root (or framework equivalent). Notable cases: - Payment gateway / Digital wallet — injection of network clients, KMS, ledger. - Logger framework — appenders + formatters injected at config time. - A/B testing platform / Feature flags — injection of evaluator + storage. - Job scheduler — injection of executor + retry policy + clock.

Glossary & fundamentals#

Concepts referenced in this design. Each row links to its canonical page; the tag column shows whether it is a high-level (HLD) or low-level (LLD) concept.

Tag Concept What it is Page
HLD Idempotency & retries safe re-execution, backoff + jitter idempotency-retries
LLD Creational patterns Singleton, Factory, Builder, Prototype creational-patterns
LLD Structural patterns Adapter, Decorator, Facade, Proxy, Composite structural-patterns
LLD Dependency injection DI / IoC, constructor injection, containers dependency-injection
LLD Immutability immutable types, persistent collections immutability