Skip to content

Dependency Injection / IoC#

flowchart LR
  Caller[App composition root]
  Service[Service]
  DepA([Dependency A])
  DepB([Dependency B])
  Caller -->|new with deps| Service
  Caller -->|new| DepA
  Caller -->|new| DepB
  Service -.uses.-> DepA
  Service -.uses.-> DepB

  classDef p fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
  classDef s fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
  classDef r fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
  class Caller p;
  class Service s;
  class DepA,DepB r;

    classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
    classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
    classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
    classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
    classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
    classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
    classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
    classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
    class Caller,Service,DepA,DepB service;

A class should receive its collaborators, not create them. The component that wires everything together is the "composition root" - usually main() or a DI container's config.

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

Quick reference#

When DI pays off#

  • Testing - swap real for fake / mock.
  • Multiple deployments - same code, different config (cloud A vs B).
  • Plug-in architectures - load implementations at startup.
  • Cross-cutting concerns - inject logging / metrics / tracing.

When it's overkill#

  • Trivial CLI tools.
  • Pure functions without external state.

Useful rules#

  1. Constructor injection by default.
  2. Don't pass the container around; pass the things.
  3. One composition root.
  4. Avoid runtime reflection magic when compile-time injection works (Dagger > Spring runtime for performance).
  5. Lifecycle scopes match logical boundaries (request, transaction, batch).

DI vs IoC#

  • Inversion of Control (IoC): a framework calls you, you don't call the framework.
  • Dependency Injection: a specific IoC technique focusing on how dependencies arrive.
  • All DI is IoC; not all IoC is DI (e.g. event loops are IoC without DI).

Refs#

  • Martin Fowler: "Inversion of Control Containers and the Dependency Injection pattern" (2004).
  • Mark Seemann: Dependency Injection in .NET (book, but principles are language-agnostic).
  • Google Dagger user guide (compile-time DI).

FAQ#

What is dependency injection in simple terms?#

Dependency injection means a class receives its collaborators from outside instead of creating them itself. The caller wires the graph at startup so classes stay testable.

What is the difference between IoC and dependency injection?#

Inversion of Control is the broad principle that the framework controls flow. Dependency injection is one concrete way to apply IoC by handing dependencies into a class.

Why is constructor injection usually preferred?#

Constructor injection makes dependencies explicit, supports final or read-only fields, and lets the compiler catch missing collaborators. Setter injection allows partially constructed objects.

Do I need a DI framework like Spring or Guice?#

No. For small apps, manual wiring in a composition root is clear and fast. Frameworks help when the graph becomes large or you need lifecycle and scope management.

How does dependency injection improve testing?#

Tests can pass in fakes, stubs, or mocks instead of real databases or HTTP clients. The unit under test never touches infrastructure during tests.

  • SOLID Principles: DIP is the principle DI implements; SRP keeps injected components focused
  • Clean Architecture: DI is the wiring mechanism for clean/hexagonal ports and adapters
  • Repository Pattern: repositories are typically injected as dependencies into application services

Further reading#

Curated, high-credibility sources for going deeper on this topic.