Spring BootBean Scopes
Spring Boot

Bean Scopes

Bean scope determines how many instances of a bean Spring creates and how long each instance lives. Spring provides six built-in scopes — singleton, prototype, request, session, application, and websocket. Choosing the wrong scope causes subtle bugs that are hard to reproduce and diagnose. Understanding each scope's behavior, lifecycle, and injection rules is essential for correct Spring applications.

What Is a Bean Scope?

A bean scope controls two things: how many instances of a bean the Spring IoC container creates, and how long each instance lives. Spring creates beans with a scope — if you don't declare one, the default is singleton. Choosing the wrong scope causes real bugs. A singleton bean holding mutable per-user state is shared across all users — a serious data leak. A prototype bean injected into a singleton effectively becomes a singleton because injection happens once at startup. A request-scoped bean injected directly into a singleton causes NullPointerException outside a web request. Spring provides six built-in scopes. The first two (singleton and prototype) are available everywhere. The remaining four (request, session, application, websocket) require a web application context.

Singleton Scope — The Default

Singleton is the default scope. Spring creates exactly one instance of the bean per ApplicationContext and shares that single instance with every bean that requests it. This is the scope for stateless service classes, repositories, and configuration objects.
Java
// Singleton is the default — no annotation needed:
@Service
public class UserService {
    // One instance created at startup
    // Same instance injected everywhere UserService is needed
    // Lives for the entire application lifetime
    // @PostConstruct called once, @PreDestroy called once on shutdown
}

// Explicit declaration — identical to the default:
@Service
@Scope("singleton")
public class UserService { }

// Or using the constant:
@Service
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public class UserService { }

// Singleton means ONE instance per ApplicationContext:
// In a standard Spring Boot app, there is one ApplicationContext
// → one UserService instance shared by the entire application
// In tests with @SpringBootTest, each test gets a fresh context
// → a fresh UserService per test class (not per test method)

// Singleton beans are created eagerly at startup by default:
// All @Service, @Repository, @Component beans initialize at startup
// Use @Lazy to defer creation until first use:
@Service
@Lazy
public class ReportingService {
    // Created only when first injected or requested
}

// When to use singleton:
// - Stateless services (most service classes)
// - Repositories (no per-user state)
// - Configuration objects
// - Infrastructure beans (connection pools, HTTP clients)
// - Basically: anything that doesn't hold per-user or per-request state

// IMPORTANT: Singleton beans must be thread-safe:
@Service
public class CounterService {
    private int count = 0;          // DANGEROUS — shared mutable state
    public void increment() { count++; }  // race condition in concurrent requests
}

@Service
public class ThreadSafeCounterService {
    private final AtomicInteger count = new AtomicInteger(0);  // thread-safe
    public void increment() { count.incrementAndGet(); }
}

Prototype Scope — New Instance Every Time

Prototype scope creates a new bean instance every time it is requested from the container — whether through injection, getBean(), or any other means. Spring hands over the instance and takes no further responsibility for it.
Java
// Prototype scope — new instance on every request:
@Component
@Scope("prototype")
public class ReportGenerator {
    private final List<String> lines = new ArrayList<>();
    private String reportTitle;

    public void setTitle(String title) { this.reportTitle = title; }
    public void addLine(String line) { this.lines.add(line); }
    public String generate() {
        return reportTitle + "\n" + String.join("\n", lines);
    }
}

// Every getBean() call or @Autowired injection creates a new instance:
ApplicationContext context = ...;
ReportGenerator r1 = context.getBean(ReportGenerator.class);
ReportGenerator r2 = context.getBean(ReportGenerator.class);
System.out.println(r1 == r2);   // false — different instances

// CRITICAL: Spring does NOT call @PreDestroy on prototype beans:
@Component
@Scope("prototype")
public class DatabaseCursor {
    private Connection connection;

    @PostConstruct
    public void open() {
        connection = dataSource.getConnection();  // @PostConstruct IS called
    }

    @PreDestroy
    public void close() {
        connection.close();   // @PreDestroy is NOT called by Spring
        // YOU must close this manually — Spring forgets about prototypes after injection
    }
}

// The prototype-into-singleton problem:
@Service  // singleton
public class OrderProcessor {

    @Autowired
    private ReportGenerator reportGenerator;  // prototype — but injected ONCE at startup
    // Every call to orderProcessor.process() uses the SAME ReportGenerator
    // Prototype scope is effectively lost — this is a SINGLETON-behaving bean!

    public String process(Order order) {
        reportGenerator.setTitle("Order: " + order.getId());  // shared state — BUG
        reportGenerator.addLine("Item: " + order.getItem());
        return reportGenerator.generate();  // may contain data from previous orders!
    }
}

// FIX 1 — Inject ApplicationContext and look up a fresh instance each time:
@Service
public class OrderProcessor {
    private final ApplicationContext context;

    public OrderProcessor(ApplicationContext context) {
        this.context = context;
    }

    public String process(Order order) {
        ReportGenerator generator = context.getBean(ReportGenerator.class); // new each time
        generator.setTitle("Order: " + order.getId());
        generator.addLine("Item: " + order.getItem());
        return generator.generate();
    }
}

// FIX 2 — ObjectFactory or ObjectProvider (cleaner):
@Service
public class OrderProcessor {
    private final ObjectProvider<ReportGenerator> generatorProvider;

    public OrderProcessor(ObjectProvider<ReportGenerator> generatorProvider) {
        this.generatorProvider = generatorProvider;
    }

    public String process(Order order) {
        ReportGenerator generator = generatorProvider.getObject(); // new each time
        generator.setTitle("Order: " + order.getId());
        return generator.generate();
    }
}

// FIX 3 — Lookup method injection (Spring creates a subclass via CGLIB):
@Service
public abstract class OrderProcessor {

    public String process(Order order) {
        ReportGenerator generator = createReportGenerator(); // new each time
        generator.setTitle("Order: " + order.getId());
        return generator.generate();
    }

    @Lookup   // Spring overrides this method to return a new prototype each call
    protected abstract ReportGenerator createReportGenerator();
}

Request Scope — One Per HTTP Request

Request scope creates a new bean instance for every HTTP request. The instance is created when the request starts and destroyed when the request ends. Ideal for holding per-request data like the current user, request ID, or request-level cache.
Java
// Request scope — one instance per HTTP request:
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST,
       proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {

    private final String requestId = UUID.randomUUID().toString();
    private final long startTime = System.currentTimeMillis();
    private String currentUserId;
    private Map<String, Object> attributes = new HashMap<>();

    public String getRequestId() { return requestId; }
    public long getElapsedMs() { return System.currentTimeMillis() - startTime; }
    public void setCurrentUserId(String id) { this.currentUserId = id; }
    public String getCurrentUserId() { return currentUserId; }
    public void setAttribute(String key, Object value) { attributes.put(key, value); }
    public Object getAttribute(String key) { return attributes.get(key); }
}

// proxyMode = ScopedProxyMode.TARGET_CLASS is REQUIRED when injecting
// a request-scoped bean into a singleton:
@Service  // singleton
public class UserService {

    private final RequestContext requestContext;  // request-scoped — injected via proxy

    public UserService(RequestContext requestContext) {
        this.requestContext = requestContext;
        // requestContext is a CGLIB proxy, not the real object
        // Each method call on requestContext delegates to the real
        // RequestContext bean for the CURRENT HTTP request
    }

    public User getCurrentUser() {
        String userId = requestContext.getCurrentUserId();  // delegates to current request's instance
        return userRepository.findById(Long.parseLong(userId)).orElseThrow();
    }
}

// Request scope in an interceptor — set per-request data early:
@Component
public class RequestTrackingInterceptor implements HandlerInterceptor {

    private final RequestContext requestContext;

    public RequestTrackingInterceptor(RequestContext requestContext) {
        this.requestContext = requestContext;
    }

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) {
        requestContext.setCurrentUserId(extractUserId(request));
        response.setHeader("X-Request-Id", requestContext.getRequestId());
        return true;
    }
}

// Without proxyMode — only valid when RequestContext is injected
// into another request-scoped bean or used inside a request directly:
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST)  // no proxyMode
public class RequestValidator {
    // Only inject this into other request-scoped beans or @RestController methods
}

Session Scope — One Per HTTP Session

Session scope creates one bean instance per HTTP session. The instance persists across multiple requests from the same user and is destroyed when the session expires or is invalidated. Used for storing user-specific state across requests — shopping carts, user preferences, wizard state.
Java
// Session scope — one instance per HTTP session:
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION,
       proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ShoppingCart {

    private final List<CartItem> items = new ArrayList<>();
    private String couponCode;

    public void addItem(CartItem item) {
        items.stream()
             .filter(i -> i.getProductId().equals(item.getProductId()))
             .findFirst()
             .ifPresentOrElse(
                 existing -> existing.incrementQuantity(item.getQuantity()),
                 () -> items.add(item)
             );
    }

    public void removeItem(Long productId) {
        items.removeIf(i -> i.getProductId().equals(productId));
    }

    public void applyCoupon(String code) { this.couponCode = code; }
    public List<CartItem> getItems() { return Collections.unmodifiableList(items); }

    public BigDecimal getTotal() {
        return items.stream()
                    .map(CartItem::getSubtotal)
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}

// Inject into a controller — proxy handles session binding:
@RestController
@RequestMapping("/api/cart")
public class CartController {

    private final ShoppingCart shoppingCart;  // session-scoped proxy

    public CartController(ShoppingCart shoppingCart) {
        this.shoppingCart = shoppingCart;
    }

    @GetMapping
    public CartResponse getCart() {
        return CartResponse.from(shoppingCart);
    }

    @PostMapping("/items")
    @ResponseStatus(HttpStatus.CREATED)
    public void addItem(@RequestBody @Valid AddToCartRequest request) {
        shoppingCart.addItem(new CartItem(request.productId(), request.quantity()));
    }

    @DeleteMapping("/items/{productId}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void removeItem(@PathVariable Long productId) {
        shoppingCart.removeItem(productId);
    }

    @PostMapping("/checkout")
    public OrderResponse checkout() {
        List<CartItem> items = shoppingCart.getItems();
        // process order...
        shoppingCart.getItems().clear();  // empty cart after checkout
        return orderResponse;
    }
}

// Session scope and security — clear session data on logout:
@Component
public class LogoutHandler implements org.springframework.security.web.authentication.logout.LogoutHandler {

    @Override
    public void logout(HttpServletRequest request,
                       HttpServletResponse response,
                       Authentication authentication) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            session.invalidate();  // destroys all session-scoped beans for this session
        }
    }
}

Application Scope — One Per ServletContext

Application scope creates a single bean instance for the entire web application — shared across all requests and sessions. Similar to singleton but scoped to the ServletContext rather than the ApplicationContext. Used for application-wide shared mutable state.
Java
// Application scope — one instance per ServletContext:
@Component
@Scope(value = WebApplicationContext.SCOPE_APPLICATION,
       proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ApplicationStatistics {

    private final AtomicLong totalRequests = new AtomicLong(0);
    private final AtomicLong activeUsers = new AtomicLong(0);
    private final ConcurrentHashMap<String, Long> endpointHits = new ConcurrentHashMap<>();
    private volatile LocalDateTime startTime = LocalDateTime.now();

    public void recordRequest(String endpoint) {
        totalRequests.incrementAndGet();
        endpointHits.merge(endpoint, 1L, Long::sum);
    }

    public void userConnected() { activeUsers.incrementAndGet(); }
    public void userDisconnected() { activeUsers.decrementAndGet(); }

    public long getTotalRequests() { return totalRequests.get(); }
    public long getActiveUsers() { return activeUsers.get(); }
    public LocalDateTime getStartTime() { return startTime; }
    public Map<String, Long> getEndpointHits() {
        return Collections.unmodifiableMap(endpointHits);
    }
}

// Application scope vs Singleton:
// Singleton: one per Spring ApplicationContext
//            → in a standard app: same as application scope
//            → in tests: fresh instance per test context
// Application: one per ServletContext
//            → tied to the web application lifecycle
//            → survives ApplicationContext refresh (context.refresh())
//            → visible via ServletContext.getAttribute()

// Use application scope when:
// - You need the bean accessible via ServletContext (legacy frameworks)
// - You want the bean to survive ApplicationContext refresh
// - You're storing truly application-wide counters or state
// For most cases, singleton is simpler and equivalent

WebSocket Scope

WebSocket scope creates one bean instance per WebSocket session. The bean lives for the duration of the WebSocket connection and is destroyed when the connection closes. Used in real-time applications that maintain per-connection state.
Java
// WebSocket scope — one instance per WebSocket session:
@Component
@Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class WebSocketSessionData {

    private String userId;
    private String sessionId = UUID.randomUUID().toString();
    private LocalDateTime connectedAt = LocalDateTime.now();
    private final List<String> subscribedTopics = new ArrayList<>();

    public void setUserId(String userId) { this.userId = userId; }
    public String getUserId() { return userId; }
    public String getSessionId() { return sessionId; }
    public void subscribe(String topic) { subscribedTopics.add(topic); }
    public List<String> getSubscribedTopics() {
        return Collections.unmodifiableList(subscribedTopics);
    }
}

// Using WebSocket scope in a message handler:
@Controller
public class ChatController {

    private final WebSocketSessionData sessionData;
    private final SimpMessagingTemplate messagingTemplate;

    public ChatController(WebSocketSessionData sessionData,
                          SimpMessagingTemplate messagingTemplate) {
        this.sessionData = sessionData;
        this.messagingTemplate = messagingTemplate;
    }

    @MessageMapping("/subscribe")
    public void subscribe(SubscribeRequest request, Principal principal) {
        sessionData.setUserId(principal.getName());
        sessionData.subscribe(request.getTopic());
    }

    @MessageMapping("/chat.send")
    @SendTo("/topic/messages")
    public ChatMessage sendMessage(ChatMessage message) {
        message.setSenderId(sessionData.getUserId());
        message.setSessionId(sessionData.getSessionId());
        return message;
    }
}

Custom Scope

Spring allows you to define your own scope — a custom rule for how many instances are created and when they live. Implement the Scope interface and register it with the container.
Java
// Custom scope — one instance per tenant in a multi-tenant application:
public class TenantScope implements Scope {

    private final Map<String, Map<String, Object>> tenantBeans = new ConcurrentHashMap<>();

    private String getCurrentTenantId() {
        // Retrieve from thread-local, request header, JWT token, etc.
        return TenantContext.getCurrentTenantId();
    }

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        String tenantId = getCurrentTenantId();
        Map<String, Object> beans = tenantBeans.computeIfAbsent(
            tenantId, id -> new ConcurrentHashMap<>()
        );
        return beans.computeIfAbsent(name, n -> objectFactory.getObject());
    }

    @Override
    public Object remove(String name) {
        String tenantId = getCurrentTenantId();
        Map<String, Object> beans = tenantBeans.get(tenantId);
        return beans != null ? beans.remove(name) : null;
    }

    @Override
    public void registerDestructionCallback(String name, Runnable callback) {
        // Register callback to be called when the tenant scope ends
    }

    @Override
    public Object resolveContextualObject(String key) { return null; }

    @Override
    public String getConversationId() { return getCurrentTenantId(); }
}

// Register the custom scope:
@Configuration
public class TenantScopeConfig {

    @Bean
    public static CustomScopeConfigurer customScopeConfigurer() {
        CustomScopeConfigurer configurer = new CustomScopeConfigurer();
        configurer.addScope("tenant", new TenantScope());
        return configurer;
    }
}

// Use the custom scope:
@Component
@Scope("tenant")
public class TenantDataCache {
    private final Map<String, Object> cache = new HashMap<>();

    public void put(String key, Object value) { cache.put(key, value); }
    public Object get(String key) { return cache.get(key); }
}
// Each tenant gets their own TenantDataCache instance
// Requests from the same tenant share the same instance

Scoped Proxy — Injecting Narrow into Wide

Injecting a narrower-scoped bean (request, session, prototype) into a wider-scoped bean (singleton) requires a scoped proxy. Without it, the singleton captures a single instance at startup — defeating the purpose of the narrower scope.
Java
// The problem — without proxyMode:
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST)  // no proxy
public class RequestLogger {
    private String requestId = UUID.randomUUID().toString();
    public String getRequestId() { return requestId; }
}

@Service  // singleton
public class AuditService {
    private final RequestLogger requestLogger;  // request-scoped — injected ONCE at startup

    public AuditService(RequestLogger requestLogger) {
        this.requestLogger = requestLogger;
        // requestLogger is the bean created during application startup
        // (before any requests arrive) — or throws ScopeNotActiveException
    }

    public void logAction(String action) {
        String id = requestLogger.getRequestId();  // ALWAYS the same id — WRONG
    }
}

// The fix — proxyMode creates a proxy that delegates to the current scope's instance:
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST,
       proxyMode = ScopedProxyMode.TARGET_CLASS)    // CGLIB proxy for classes
public class RequestLogger {
    private String requestId = UUID.randomUUID().toString();
    public String getRequestId() { return requestId; }
}

// For interfaces, use INTERFACES proxy mode:
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION,
       proxyMode = ScopedProxyMode.INTERFACES)      // JDK proxy for interfaces
public class UserSessionImpl implements UserSession { }

// Now AuditService works correctly:
@Service
public class AuditService {
    private final RequestLogger requestLogger;  // actually a CGLIB proxy

    public AuditService(RequestLogger requestLogger) {
        this.requestLogger = requestLogger;
        // requestLogger is a proxy object
        // Every call to requestLogger.getRequestId() delegates to
        // the REAL RequestLogger for the CURRENT request
    }

    public void logAction(String action) {
        String id = requestLogger.getRequestId();  // correct — current request's id
        auditRepository.save(new AuditEntry(action, id));
    }
}

// What proxyMode does internally:
// 1. Creates a CGLIB subclass (or JDK proxy for interfaces) of RequestLogger
// 2. Injects that proxy everywhere RequestLogger is needed
// 3. Every method call on the proxy looks up the current scope
//    (current HTTP request's ThreadLocal, current HTTP session, etc.)
// 4. Delegates the method call to the real bean for that scope
// 5. The proxy itself is a singleton — but it delegates to scope-specific instances

Scope Reference Summary

A complete reference of all six scopes, their availability, instance count, lifecycle, and typical use cases.
Java
// Scope        Available in    Instances           Lifecycle
// ────────────────────────────────────────────────────────────────────────────
// singleton    Any context     1 per AppContext    App startup → App shutdown
// prototype    Any context     New each request    Created on demand, never destroyed by Spring
// request      Web only        1 per HTTP req      HTTP request start → HTTP request end
// session      Web only        1 per HTTP session  Session create → Session expire/invalidate
// application  Web only        1 per ServletCtx    App start → App stop
// websocket    Web only        1 per WS session    WS connect → WS disconnect

// Scope usage examples:
// singleton   → UserService, OrderRepository, JwtTokenService, DataSource
// prototype   → ReportGenerator (stateful, one-use object), Builder classes
// request     → RequestContext (requestId, timing), RequestValidator
// session     → ShoppingCart, UserPreferences, WizardState (multi-step form)
// application → ApplicationStatistics, ApplicationVersionInfo
// websocket   → WebSocketSessionData, per-connection subscriptions

// proxyMode required when injecting into singleton:
// request  → proxyMode = TARGET_CLASS (or INTERFACES)
// session  → proxyMode = TARGET_CLASS (or INTERFACES)
// application → proxyMode = TARGET_CLASS (singleton-like, rarely needed)
// prototype → use ObjectProvider or @Lookup instead of proxyMode

// Thread safety requirements:
// singleton    → MUST be thread-safe (concurrent requests)
// prototype    → typically not needed (each caller has own instance)
// request      → not needed (one request = one thread in servlet model)
// session      → MAY need thread safety (tabs in same browser = same session)
// application  → MUST be thread-safe (same as singleton)