(Mis-)Use Spring proxy magic to inject Http request into business layer - should you?
Source: Dev.to
A Request‑Scoped Bean Inside a Singleton – Why It Feels Wrong
A few weeks ago, while reviewing a Spring Boot codebase, I found a service that injected HttpServletRequest as a field into a singleton bean. The bean is called from the service layer to construct and publish domain events, where the events need information from the incoming request.
@Component
public class EventFactory {
@Autowired
private HttpServletRequest request;
public void prepareAndSendEvent() {
OrderEvent ordEvent = new OrderEvent();
ordEvent.setSentClientIP(request.getRemoteAddr());
}
}
At first glance the code works and looks convenient, but a request‑scoped concern is quietly living inside a singleton bean, and a transport‑layer abstraction has leaked straight into business logic. This small design choice opens a larger conversation about how easily Spring’s magic can blur architectural boundaries if we’re not careful.
Why Does This Feel Off?
-
Bean scopes don’t match.
EventFactoryis a singleton – Spring creates it once at startup and re‑uses the same instance for every HTTP request.
HttpServletRequestis request‑scoped – a new instance is created for each incoming request. -
How can Spring inject a request‑scoped bean into a singleton?
During startup there is no HTTP request, yet Spring still manages to wire the field. It does this by injecting a scoped proxy that delegates to the actual request object at runtime.
Bridging the Difference in Scope
When Spring sees a request‑scoped dependency in a singleton, it creates a proxy for the dependency. The proxy holds a reference to an ObjectFactory that retrieves the current HttpServletRequest from a ThreadLocal (or, in newer versions, a scoped context) whenever a method on the proxy is invoked.
Thus, when prepareAndSendEvent() accesses request.getRemoteAddr(), the proxy looks up the current request and forwards the call to it. This is Spring’s standard way of handling scope mismatches.
Why Is This a Smell?
Spring’s “one request per thread” model and its stereotype annotations (@Component, @Service, @Repository) make a three‑layer architecture easy to achieve:
- Controller layer – receives the HTTP request, builds DTOs, and delegates to the service layer.
- Service layer – contains stateless business logic, works only with DTOs.
- Repository/gateway layer – interacts with persistence or external systems.
Injecting HttpServletRequest directly into a service (or any business bean) breaks this separation of concerns. The transport layer now leaks into the business layer, making the code harder to understand, test, and evolve.
The price we pay is the silent degradation of clean code and compromise of layered architecture for very little convenience.
What Problems Can This Cause?
1. Falls Apart in Asynchronous Execution
The proxy works because the call happens synchronously on the same thread that holds the request‑scoped ThreadLocal. If the method is later executed asynchronously (e.g., via @Async, CompletableFuture, or a task executor), the ThreadLocal is not available, leading to an IllegalStateException.
2. Testing Complexity Increases
Unit‑testing EventFactory now requires a mocked HttpServletRequest. Mocking request objects is usually reserved for controller tests, not for pure business‑logic tests. This forces tests to focus on plumbing rather than behavior, reducing their value.
Recommended Approach
Extract the needed request data outside the business layer and pass it as a method argument (or a dedicated DTO). This keeps the business beans completely unaware of the HTTP layer.
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderService orderService;
@GetMapping
public List getOrders(HttpServletRequest request) {
String clientIp = request.getRemoteAddr();
return orderService.getOrders(clientIp);
}
}
@Service
public class OrderService {
private final EventFactory eventFactory;
public List getOrders(String clientIp) {
// business logic …
eventFactory.prepareAndSendEvent(clientIp);
// …
}
}
@Component
public class EventFactory {
public void prepareAndSendEvent(String clientIp) {
OrderEvent ordEvent = new OrderEvent();
ordEvent.setSentClientIP(clientIp);
// publish event …
}
}
Now:
- Scope boundaries are respected – the service layer receives only plain data.
- Asynchronous execution is safe – no hidden
ThreadLocaldependencies. - Unit tests stay simple – you can test
EventFactorywithout any servlet API mocks.
Visual Summary

TL;DR
- Injecting a request‑scoped bean (
HttpServletRequest) into a singleton is technically possible thanks to Spring’s scoped proxies, but it violates layered architecture. - It introduces hidden runtime dependencies, breaks when you move to asynchronous execution, and makes unit testing harder.
- Extract the needed data in the controller (or a dedicated interceptor) and pass it explicitly to the service layer. This keeps your code clean, testable, and future‑proof.
Code Example
@RequestHeader("X-Tenant-Id") String tenantId) {
String userAgent = request.getHeader("User-Agent");
String clientIp = request.getRemoteAddr();
RequestContext ctx = new RequestContext(tenantId, clientIp);
return orderService.getOrders(ctx);
}
}
@Component
public class EventFactory {
public void prepareAndSendEvent(EventInfo eventInfo, RequestContext requestContext) {
// requestContext object has the needed information
}
}
TL;DR
Injecting a request‑scoped HttpServletRequest into the business layer works because of Spring magic, but it carries hidden costs and risks. It is the developer’s responsibility to avoid using it when a cleaner alternative is available.