(Mis-)Use Spring proxy magic to inject Http request into business layer - should you?

Published: (February 1, 2026 at 09:22 AM EST)
4 min read
Source: Dev.to

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.
    EventFactory is a singleton – Spring creates it once at startup and re‑uses the same instance for every HTTP request.
    HttpServletRequest is 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:

  1. Controller layer – receives the HTTP request, builds DTOs, and delegates to the service layer.
  2. Service layer – contains stateless business logic, works only with DTOs.
  3. 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.

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 ThreadLocal dependencies.
  • Unit tests stay simple – you can test EventFactory without any servlet API mocks.

Visual Summary

Layered architecture diagram

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.

Back to Blog

Related posts

Read more »

Java Virtual Threads — Quick Guide

Java Virtual Threads — Quick Guide Java 21+ · Spring Boot 3.2+ · Project Loom A concise, production‑focused guide to Java Virtual Threads — what they are, how...

MVP architecture adapted for Unity

Intro This article explains how we adopted an MVP Model–View–Presenter architecture for our Unity game. I’ll walk through the overall structure, explain the ma...

SPRING BOOT EXCEPTION HANDLING

Java & Spring Boot Exception Handling Notes 1. What is Exception? Exception = unwanted situation that breaks normal flow of program. Goal of exception handling...

Tomcat Server - Servlet Container

What is a Servlet? In Java web development, a Servlet is a Java class that: - Receives an HTTP request - Returns an HTTP response Typical flow: Browser → Servl...