skip to content

What does a dependency-injection container do in a server-side web framework, and how does a request handler get its collaborators?

level: juniorimportance: must knowfreq 72%

answer

  1. who builds the handler's collaborators?
  2. a registry of recipes, not objects
  3. registration records, resolution constructs
  4. dependencies declared as constructor parameters
  5. resolution walks the graph transitively

basics

~20 s

A dependency-injection container is a registry of construction recipes: you register how each service is built, then ask for one and it builds the whole graph behind it. Handlers declare collaborators as constructor parameters and the container supplies them.

solid answer

~40 s

A container is a registry of construction recipes. At wiring time you register each service — the key it is resolved under and how to build it. At resolution time you ask for one object, and the container walks the graph depth-first, building every collaborator it transitively needs, and returns the finished object. Handlers take part through **constructor injection**: they list their collaborators as constructor parameters, and the framework asks the container for the handler rather than the handler looking anything up. Construction moves out of the class that uses a collaborator and into one place that knows the whole graph. A hand-written startup function does exactly the same job; the container automates it and reports a missing or ambiguous binding by name.

go deeper

for a junior

Be able to say it in one breath: register how things are built, ask for one, get it with everything it needs attached. Know that a handler declares collaborators as constructor parameters instead of creating them.

for a middle

Explain the two phases separately — registration stores a key and a recipe, resolution walks the graph depth-first and constructs. Name the three failures that walk can hit: missing binding, ambiguous binding, cycle.

for a senior

Show that you know the container is optional automation. Compare it with a hand-written composition root on error timing and readability, and be ready to say when the extra indirection is not paying for itself.

for a principal

Frame it as where construction knowledge lives and when a team should accept runtime-resolved wiring over wiring the compiler can check. Discuss what a large registry costs a reader who must trace one object's origin.

## Why construction needs an owner A request handler rarely works alone. It talks to a repository, a mail sender, a clock, a configuration object. Something has to decide **which** repository, built with **which** connection, and something has to actually call its constructor. If the handler does that itself, two jobs live in one place: *doing the work*, and *deciding what the work is done with*. Pulling the second job out is the whole idea behind a **dependency-injection container**. A container is a registry of **construction recipes**. You tell it, once, how every service in the application is built; later you ask it for one object and it hands back that object with all of its collaborators already attached. ## What registration actually stores Registration is a bookkeeping step, not a construction step. In most containers a registration records: - a **key** the service will be looked up under — usually the abstraction (the interface or base type) the service satisfies, sometimes that abstraction plus a name; - a **recipe** — a constructor to invoke, a factory function to run, or an already-built instance to hand back unchanged; - a **lifetime setting** that decides whether the built object is cached and for how long. The important consequence: registering a service does **not** build it. At the end of wiring you own a description of the object graph, not the graph itself. ## What resolution does Resolution is a depth-first walk of a directed graph whose nodes are recipes and whose edges are the parameters those recipes need: 1. look the requested key up in the registry; 2. read the recipe's own parameters, and resolve each of them the same way, recursively; 3. invoke the constructor or factory with the resolved arguments; 4. cache the result, or not, according to the lifetime; 5. return the finished object to the caller. Every classic wiring failure is a fact about that walk. A **missing binding** is a key with no recipe. An **ambiguous binding** is one key with two recipes and no way to choose. A **cycle** is a path that arrives back where it started, so there is no object that can be built first. ## How a handler takes part The usual arrangement is **constructor injection**: the handler lists what it needs as constructor parameters and stores them in fields. It never names a concrete implementation, never reads a global, and never calls the container. That last point matters — the framework asks the container for the handler when a request needs one, so the handler's dependencies are visible in its signature. Anyone reading the class, and any test constructing it directly, sees the full list. The alternative — the handler calling a lookup function for each collaborator inside its body — is usually called a **service locator**. It works, but it hides the dependency list, makes a missing binding surface at call time rather than construction time, and removes the compiler's help. ## Container versus hand-written wiring Both approaches do the same job. A startup function that constructs every object in order, passing each one into the next, is a perfectly valid object graph; some teams deliberately keep one. | Concern | Hand-written composition root | Container | |---|---|---| | Adding a dependency | Edit each construction site by hand | Add one registration | | Error detection | Usually at compile time, where written | At resolution — startup or first use | | Reading the graph | Follow the function top to bottom | Read the registrations, then infer edges | | Repetition | Grows with the number of services | Roughly constant | | Failure message | A normal compile or startup error | A resolution error naming the key and path | A container is worth most when the graph is large and changes often, and worth least when it is small and stable. ## What a container does not give you - It does **not** decouple anything on its own. A handler that names a concrete type in its constructor is just as coupled when the container supplies it. - It does **not** make a tangled graph healthy. If the cycle is in the design, the container only reports it. - It is **not** a global bag of objects to read from anywhere. Used that way it degrades into a service locator and loses the property that made it useful: dependencies stated up front. Read the container as an automation of wiring you could otherwise write by hand — nothing more, and nothing less.

  • Why is constructor injection usually preferred over setting collaborators on an already-constructed object?
    Because the object is never observable in a half-built state. Every collaborator arrives before the first method call, the field can stay immutable, and the constructor signature is an honest, compiler-checked list of what the class needs. Assignment after construction hides that list and allows a call to land on a missing collaborator.
  • What changes if a handler calls the container for a collaborator instead of declaring it?
    That is the service-locator style. The dependency list disappears from the signature, so readers and tests cannot see it; the container becomes a runtime prerequisite of the class; and a missing binding surfaces when that line runs rather than when the handler is built. It works, but it trades early, visible failure for late, hidden failure.
  • Does using a container make code loosely coupled?
    No. Coupling is decided by what the constructor parameters name. A handler that takes a concrete implementation is tightly coupled to it whoever calls the constructor. The container changes who performs construction, not what the class depends on; depending on an abstraction is a separate, deliberate design choice.

A container is a parts catalogue rather than a warehouse: it stores the build instructions for each part, and only assembles a finished unit when someone orders one.

saying these in an interview costs you the question

  • Thinks registering a service immediately constructs it
  • Treats the container as a global bag read from anywhere in the code
  • Says using a container by itself makes code loosely coupled
  • Cannot say how the container knows which implementation satisfies an abstraction
  • Believes hand-written wiring cannot share one instance between several consumers