DI Lifetime Scenarios
Dependency injection lifetimes are a favorite interview topic, because the definitions are easy and the failure modes are subtle. Expect definitions as a warm-up, then a scenario built around a captive dependency.
The warm-up, answered with intent
"Singleton, scoped, transient β go." Don't just define; attach the use case to each:
Singleton β one instance for the process lifetime; for stateless, thread-safe services
(configuration wrappers, HttpClient-backed API clients, caches). Scoped β one instance
per request; the natural home of DbContext and anything carrying per-request state.
Transient β new instance every resolution; for lightweight, stateless helpers. The
sentence that upgrades the answer: "lifetime is really a statement about state and
thread-safety β a singleton must be safe for concurrent requests; scoped things must
never outlive their request."
The captive dependency scenario
"A background-ish singleton service takes a DbContext in its constructor. It works on the
dev box. In production you get intermittent ObjectDisposedException and cross-request data
weirdness. What happened?"
This is the captive dependency β the classic. Reason it aloud: the singleton is
constructed once and holds ("captures") whatever was injected β so the scoped DbContext
from the first resolution lives forever inside it. Consequences: the context's scope ends
and disposal makes later calls throw; worse, DbContext is not thread-safe, and a
singleton is hit by concurrent requests, so you can also get corrupted change-tracker state
and one user's data bleeding into another's query. It "works" locally because one developer
clicking around never overlaps requests.
How to spot it: any longer-lived service depending on a shorter-lived one β singleton β
scoped is the deadly pair. The default DI container validates scopes in Development
(ValidateScopes), which is why the honest verification answer is "this often would have
thrown on startup in dev β I'd check whether validation was disabled or the resolution went
through a root-scope path like a hosted service."
How to fix it: don't inject the scoped thing β inject IServiceScopeFactory, create a
scope per unit of work, resolve the DbContext inside it, dispose the scope. Six lines:
using var scope = scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await ProcessBatchAsync(db);
This is exactly the shape of every hosted/background service that touches EF β say that, because it connects the trivia to real architecture.
Answers that fall flat in this round
Claiming "make everything transient to be safe" (a per-resolution DbContext breaks
unit-of-work semantics and change tracking); asserting singletons are "faster" as the design
driver; or fixing the scenario by making DbContext a singleton β the one answer worse
than the bug.
Practice prompts:
- Explain the captive dependency to a junior in 90 seconds using the singletonβDbContext example, including why production surfaces it and dev doesn't.
- "Where does a hosted service that processes uploaded transcripts nightly get its DbContext?" Answer with the scope-factory pattern aloud.
- Pick three services from your real app and defend the lifetime each one has.