The full picture
@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.