#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.
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
- Registration stores a descriptor per service type: the implementation or a factory function, and the lifetime.
- Resolution looks up the descriptor. For a singleton it returns the cached instance if one exists.
- Otherwise it picks a constructor, resolves each parameter by calling itself, creates the object and caches it according to its lifetime.
- 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.
- 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?