All system design challenges
#21 Framework/LLD Patterns

IoC/Dependency Injection

Build a small dependency-injection container: register how to make each object, then let the container build and wire them.

Asked at ADP Google

What is being asked

This is a low-level design question. Implement the core of an inversion-of-control container: classes declare what they need, and the container creates them in the right order and hands each its dependencies.

Requirements

Functional

  • Register a type against an implementation or a factory.
  • Resolve a type, constructing its dependencies recursively.
  • Lifetimes: singleton, one per scope (such as a web request), and a new one every time.

Non-functional

  • Detect circular dependencies and report them clearly.
  • Thread-safe resolution.
  • Fast after the first resolution.

Design

Application codeContainer API:register / resolveRegistry: type todescriptorResolverSingleton cacheScope cacheInstance factory
Boxes are services; rounded shapes are data stores and queues.
  1. Registration stores a descriptor per service type: the implementation or a factory function, and the lifetime.
  2. Resolution looks up the descriptor. For a singleton it returns the cached instance if one exists.
  3. Otherwise it picks a constructor, resolves each parameter by calling itself, creates the object and caches it according to its lifetime.
  4. A set of "types being resolved" on the current call path detects a cycle: meeting a type already in the set means A needs B needs A.
  5. A scope is a child container with its own cache for scoped services; it disposes them when it ends.

Key decisions and trade-offs

  • Constructor injection makes dependencies explicit and objects valid once built. Field or setter injection hides them and allows half-built objects.
  • Reflection cost. Inspecting constructors on every call is slow; cache a compiled factory per type after the first resolution.
  • Captive dependencies. A singleton that holds a scoped service keeps it alive for ever. A good container detects and rejects this.
  • Service locator anti-pattern. Passing the container around and calling resolve inside classes hides dependencies again; only the composition root should touch the container.

Follow-up questions

  • How would you support several implementations of one interface?
  • How do you make singleton creation safe when two threads resolve it at once?
  • How would you add interception, for example logging every call?
  • Why does inversion of control make code easier to test?
Log in to tick off the challenges you have worked through. Log in