In a web framework, why does swapping a collaborator after startup often leave handlers still using the real one?
answer
- wiring is a build step, not a lookup
- references handed out at boot
- the registry leaves the path afterwards
- per-request resolution behaves differently
- construction side effects cannot be retracted
basics
~20 sBecause wiring mostly resolves once. Single-instance dependencies are constructed and handed to their consumers while the graph is built, so a later edit to the registry changes only lookups that still happen, not references already injected.
solid answer
~50 sBuilding the object graph is a one-time act for anything with a single-instance lifetime: the container constructs the collaborator, passes it into the components that declared it, and those components keep the reference. Editing the registry afterwards replaces what a *future* lookup would return, which is why the change appears to do nothing for a handler that was injected at boot. Frameworks differ here: where a handler resolves its dependency from the container on each request, or sits behind an indirection that re-resolves, a late swap does take effect for later requests — which makes the behaviour inconsistent rather than simply absent, and inconsistent is worse. Some startup work is also unrecoverable: connections opened, values read once and cached, routes registered against the instance that existed then. The reliable form is to contribute the replacement to the configuration the container reads, then let the graph be built.
go deeper
Hold on to the sequence: the application reads its registrations, builds objects, and hands them to each other. Anything you change after that point arrives too late for the objects that already exist.
Explain single-instance resolution and injected references, and say honestly where a late swap does work — per-request resolution or an indirection that forwards calls. Mention construction-time side effects that no swap can undo.
Diagnose the symptom in order: replacement declared after boot, a cached graph built without it, per-request resolution producing half-behaviour, or nothing on the path using that boundary. Say which you would check first and why.
Argue against test infrastructure whose correctness depends on a boundary's resolution style. Set the expectation that replacements are declared as configuration before boot, so tests do not silently change meaning when someone alters a lifetime.
## Wiring is a build step, not a lookup table It is tempting to picture the container as a dictionary the application consults whenever it needs a collaborator. For anything registered with a single-instance lifetime that picture is wrong, and the wrongness is exactly what this question probes. At startup the container walks the registrations, constructs instances, and **passes them into the components that declared a dependency on them**. From that moment the consumer holds a direct reference. It does not ask again. Replacing the registration later swaps what a future resolution would produce, but no future resolution is coming for a component that was wired at boot. ## What has already happened by the time the first request arrives By the end of startup a typical framework has done several things that a late swap cannot undo: 1. **Constructed and injected** the single-instance collaborators, so consumers hold references. 2. **Registered routes** against handler instances or factories captured at that moment. 3. **Composed the middleware chain**, with each stage holding whatever it was given. 4. **Run construction-time work**: pools opened, values read once and cached, listeners subscribed, warm-up performed. Items 1 and 4 are the ones that bite. Even a framework that would happily re-resolve a dependency cannot un-open the connection the real implementation made while it was being constructed. ## Where frameworks genuinely differ The honest generalisation is "often", not "always". Resolution style decides the outcome: | how the consumer gets its dependency | effect of a post-startup swap | |---|---| | injected once, single-instance | none for that consumer; it keeps the original reference | | resolved from the container per request or per invocation | later requests see the replacement; in-flight ones may not | | held behind an indirection that forwards each call | later calls see the replacement, because the reference is to the indirection | | captured inside a closure registered at startup | none, regardless of what the registry now says | A suite built on a late swap therefore behaves differently depending on which of those styles the boundary in question happens to use — and that can change when someone edits an unrelated registration. Tests whose correctness depends on an accident of resolution style are the kind that pass for a year and then fail on an unrelated refactor. ## The reliable ordering The rule that survives every style is: **contribute the replacement to the configuration the container reads, then build the graph**. Concretely, a test harness that does this well will - collect the test's requested replacements before it starts the application; - merge them over the application's own registrations, so the real candidate is not constructed at all; - boot the graph once from the merged set; - and only then hand the test a client to send requests through. Because the real implementation is never constructed, its construction-time side effects never happen either — which is frequently the actual reason the test wanted the replacement, rather than any assertion about calls. ## Diagnosing the symptom When a test reports that its replacement "did not take", check in this order: was the replacement registered before the boot, or after; was the graph already built and cached from an earlier test, so this test's amendment never applied to it; is the consumer resolving per request, in which case the swap partly works and the confusing half-behaviour is the clue; and finally, is anything on the exercised path actually depending on the boundary you replaced. The first two causes account for most reports. ## The cheapest way to avoid the question Harnesses that are pleasant to use take the replacement set as **input** to the boot rather than as an operation performed on a running application. The test declares what it wants, the harness merges those declarations over the application's registrations, boots once, and hands back a client. Nothing in the test body touches a live registry, so the ordering this whole question turns on cannot be got wrong — and the failure mode that replaces it, a cached graph built from a different set, is at least loud and easy to reason about once you know the cache is keyed on the wiring. ## What interviewers listen for A middle-tier answer states the lifecycle: registrations are read, the graph is built, references are handed out, and after that the registry is no longer in the path. A stronger answer adds the per-request and indirection cases without claiming the swap is universally ineffective, and notes the construction-time side effects that no swap can retract.
- When would a post-startup swap actually take effect, and why is that still a poor thing to rely on?It takes effect where the consumer resolves the dependency per request, or holds an indirection that forwards each call. Relying on it makes the test's correctness depend on the resolution style of that one boundary, which can change under an unrelated refactor, and it leaves in-flight work on the old reference.
- Why can replacing the registration before boot do something a post-startup swap never can?The real implementation is never constructed, so its construction-time work — opening connections, subscribing listeners, reading and caching a value, warming up — never happens. A swap performed after startup arrives too late for all of it, which is often the very side effect the test was trying to avoid.
- A replacement was declared before boot and still did not apply. What is the likely cause?The test was served by a graph that had already been built and cached from an earlier test with a different set of replacements. The declaration was read, but not for this application instance. Check what the harness keys its cached graphs on, and whether this test's set matches an existing key.
saying these in an interview costs you the question
- Thinks the container is consulted again on every call to a collaborator
- Claims a post-startup swap can never take effect anywhere
- Believes replacing a registration later undoes work done while the real one was constructed
- Registers the replacement in test setup that runs after the application has booted
- Treats inconsistent swap behaviour across boundaries as random flakiness