Card 05/ 07

GotchaDifficulty: Advanced1 min

A Request-Scoped Bean Inside a Singleton

CurrentRideContext is scoped to one request — a new instance per request, holding whatever ride that particular request is about. RideAuditor is a singleton that depends on it. Wired the obvious way, both requests below see the exact same ride.

java
@Component
@Scope("request")
class CurrentRideContext {
    private final String rideId = currentRideId();
    String describe() { return "handling " + rideId; }
}

@Component
class RideAuditor {
    private final CurrentRideContext currentRideContext;
    RideAuditor(CurrentRideContext currentRideContext) { this.currentRideContext = currentRideContext; }
    void audit() { System.out.println(Thread.currentThread().getName() + " sees: " + currentRideContext.describe()); }
}
text
request-1 auditor sees: handling ride-695
request-2 auditor sees: handling ride-695
verified — both requests, the same stale instance

RideAuditor is a singleton, so it is built exactly once — at startup, on whatever thread happens to be running then. Building it means resolving its constructor argument right then, which pins the very first CurrentRideContext that happened to exist into RideAuditor forever. Every request after that sees that one request's ride.

The fix is a scoped proxy — proxyMode = ScopedProxyMode.TARGET_CLASS on the request-scoped bean's definition. RideAuditor gets injected with a proxy instead of a real instance, and the proxy defers the actual lookup to every single call, re-resolving CurrentRideContext for whichever request is asking at that moment.

java
@Bean
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
CurrentRideContext currentRideContext() { return new CurrentRideContext(); }
text
request-1 auditor sees: handling ride-620
request-2 auditor sees: handling ride-672
verified — with the proxy, each caller resolves its own instance