Interview Prep Zoneby CuriouserLabs

Question 2 of 5 · staff level

Explain bean scopes. What goes wrong when a singleton depends on a prototype (or request-scoped) bean, and how do you fix it?

🎤 Say this first

Singleton (default, one per container), prototype (new instance per request to the container), plus web scopes: request, session, application, websocket. The trap: injection happens once, at singleton creation — so a prototype injected into a singleton is fetched once and becomes a de-facto singleton. Fixes: ObjectProvider<T>.getObject() per use, @Lookup method injection, or a scoped proxy (proxyMode = TARGET_CLASS) which injects a proxy that resolves the right instance per call — mandatory for request/session beans inside singletons.

The full picture

The stale-prototype bug and three fixes
@Component @Scope("prototype") class ReportBuilder { … }   // stateful, not thread-safe

@Service
class ReportService {
    @Autowired ReportBuilder builder;          // ❌ ONE builder, shared by all threads

    // Fix 1: provider — resolve per use
    private final ObjectProvider<ReportBuilder> builders;
    Report build() { return builders.getObject().run(); }   // ✅ fresh instance

    // Fix 2: @Lookup — container overrides this method
    @Lookup protected ReportBuilder newBuilder() { return null; }
}

// Fix 3 (required for web scopes): scoped proxy
@Component @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
class CurrentUser { … }
@Service class Audit { Audit(CurrentUser user) { … } }  // proxy resolves per request
  • Prototype ≠ per-call: it's per-getBean. Injection is a single getBean.
  • Scoped proxy mechanics: the singleton holds a CGLIB proxy; every method call looks up the live request/session instance from a ThreadLocal-backed holder — which is also why it breaks on non-request threads (@Async) unless context propagation is arranged.
  • Custom scopes are pluggable (registerScope) — tenant scope in multi-tenant systems is the classic real-world one.
  • Design smell check: needing prototype state per operation often means the object should just be a local variable, not a bean at all.

🔄 Likely follow-up questions

  • How would you implement a custom 'tenant' scope?
  • Why does a request-scoped bean throw when accessed from @Async code, and what are the options?
  • Session scope in a load-balanced deployment — what are you implicitly signing up for?