Foundation · Understand

Bean Scopes

Singleton is the default and usually right; prototype and web scopes need a reason plus a strategy for crossing scope boundaries.

Primary source: Spring Framework 6.2 Reference: Bean Scopes

15 min

Lesson 9 of 14

1d streak

The Architectural Question

A singleton holds one reference forever, so injecting a request-scoped or prototype bean freezes whatever instance happened to exist at startup.

What this guide proves

  • Singleton
  • Prototype
  • Scoped Proxies

Container scopes: Identity boundaries in memory

A bean's scope defines its lifecycle and instance reuse policy within the IoC container. Spring defines five standard scopes:

ScopeIdentity BoundaryCreation TimingDestruction Managed by Spring?
singleton (default)One instance per BeanDefinition per ApplicationContextEagerly at startup (refresh)Yes (on context shutdown)
prototypeA new instance every time requested from containerLazily on each getBean() lookupNO (Spring never calls @PreDestroy on prototypes!)
requestOne instance per HTTP request lifecycle (Web only)On demand per incoming HTTP requestYes (at end of HTTP request)
sessionOne instance per HTTP HttpSession lifecycle (Web only)On demand per HTTP sessionYes (when HttpSession is invalidated)
applicationOne instance per ServletContext (Web only)On ServletContext initializationYes (when ServletContext is destroyed)

Architectural Boundary · Do Not Confuse

Concept A

GoF Singleton Pattern (JVM-wide single instance)

Concept B

Spring Singleton Scope (Container-wide definition instance)

A classic GoF singleton restricts instantiation to one instance per classloader using a private constructor. A Spring singleton restricts instantiation to one instance per BeanDefinition within an ApplicationContext. You can easily create multiple Spring singleton instances of the same class by registering two distinct bean definitions.

The prototype scope & the memory leak trap

The most dangerous aspect of the prototype scope is that Spring does not manage the complete lifecycle of prototype beans. Spring creates the prototype instance, populates its properties, executes its BeanPostProcessor and @PostConstruct callbacks, and hands the instance over to the client. After handoff, the container retains NO reference to the prototype.

The Prototype Memory Leak Trap

Spring NEVER calls @PreDestroy or DisposableBean.destroy() on prototype beans! If your prototype bean allocates native memory, file descriptors, or database connections, relying on @PreDestroy will cause permanent resource leaks. The client code that requested the prototype must manually clean it up.

Crossing scope boundaries: Scoped proxies for web requests

What happens when a Singleton service depends on a @RequestScope bean? The singleton service is constructed once at application boot time—when NO HTTP request exists! Directly injecting the request bean into the singleton constructor would crash startup.

To resolve this, Spring uses Scoped Proxies (proxyMode = ScopedProxyMode.TARGET_CLASS). Spring injects a CGLIB proxy into the singleton. When the singleton invokes a method on the proxy, the proxy looks up the current thread's RequestAttributes and delegates to that specific HTTP request's bean instance.

UserContext.java
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserContext {
    private String userId;
    private String tenantId;
    // Proxied into singleton services cleanly!
}

Break it deliberately: The singleton-prototype mismatch bug

Directly injecting a prototype bean into a singleton service causes the prototype to be instantiated only ONCE—when the singleton is constructed. The prototype behaves like a singleton, silently breaking stateful logic.

PrototypeMismatch.java
@Component
@Scope("prototype")
public class RequestTracker {
    private final UUID requestId = UUID.randomUUID();
    public UUID getId() { return requestId; }
}

@Service
public class OrderService {
    private final RequestTracker tracker; // Injected ONCE at boot time!

    public OrderService(RequestTracker tracker) {
        this.tracker = tracker;
    }

    public void process() {
        // Tracker requestId is identical for EVERY user and request!
        System.out.println("Processing with ID: " + tracker.getId());
    }
}

The Fix: Use ObjectProvider<T>

Inject ObjectProvider<RequestTracker> trackerProvider into the singleton, and call trackerProvider.getObject() inside the method. This requests a fresh prototype instance from the container on each invocation.

Check your understanding

Active Recall Checkpoint

Why does Spring never invoke @PreDestroy lifecycle callbacks on prototype-scoped beans?

Hint: Think about object references and garbage collection.

Show answer & explanation

The container does not retain references to prototype instances after handing them to callers (to prevent memory leaks). Because the container does not hold them, it cannot track when they are discarded, leaving destruction management entirely to the caller.

Active Recall Checkpoint

How does a Scoped Proxy allow a singleton service to collaborate with a request-scoped bean?

Hint: Consider CGLIB delegation and thread-local storage.

Show answer & explanation

Spring injects a dynamic CGLIB proxy into the singleton. When methods are invoked, the proxy dynamically queries the current thread RequestAttributes to locate and delegate to the active HTTP request instance.

Runtime Architecture & Flow

Decision canvas

Evaluate the choice across three constraints

These are comparison criteria, not sequential steps. A strong answer states which constraint dominates in the current system.

CRITERION 01

Singleton

Ask how each option changes this guarantee, its operational cost, and its failure mode.

CRITERION 02

Prototype

Ask how each option changes this guarantee, its operational cost, and its failure mode.

CRITERION 03

Scoped Proxies

Ask how each option changes this guarantee, its operational cost, and its failure mode.

Decision prompt: Inject a Prototype bean into a Singleton bean and observe that the Prototype is only instantiated once (the scoped-proxy problem).

Working Implementation

Code Walkthrough

Inject a Prototype bean into a Singleton bean and observe that the Prototype is only instantiated once (the scoped-proxy problem).

ScopeBoundaryLab.java
package com.example.core;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicInteger;

public class ScopeBoundaryLab {
    // Prototype bean: fresh instance created each time requested
    @Component
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public static class RequestTraceContext {
        private static final AtomicInteger SEQ = new AtomicInteger(1);
        private final int traceId = SEQ.getAndIncrement();
        public int getTraceId() { return traceId; }
    }

    // Singleton bean holding an ObjectProvider to prevent prototype capture
    @Component
    public static class IngestionService {
        private final ObjectProvider<RequestTraceContext> traceContextProvider;

        public IngestionService(ObjectProvider<RequestTraceContext> traceContextProvider) {
            this.traceContextProvider = traceContextProvider;
        }

        public void handleEvent(String eventName) {
            // Defer resolution to runtime invocation: new prototype per call
            RequestTraceContext context = traceContextProvider.getObject();
            System.out.printf("Event: %s | TraceId: %d%n", eventName, context.getTraceId());
        }
    }
}
Singleton is the default scope. Prototype, request, and session scopes create multiple instances.
Injecting a prototype directly into a singleton freezes the first prototype forever (the scoped-proxy problem).
ObjectProvider<T> or @Scope(proxyMode = TARGET_CLASS) defers resolution so each call receives a fresh instance.

Synthesis & Production Verification

Production Acceptance Criteria

  • Demonstrate prototype-in-singleton capturing exactly one instance
  • Fix it with ObjectProvider and show a new instance per call
  • Name the built-in scopes and where request and session apply

Architectural & Technical Review

Core Interview Questions

Q01How does a Spring singleton scope differ fundamentally from the classic Gang of Four (GoF) singleton pattern?Show answer

A GoF singleton restricts instantiation to one instance per classloader via private constructors. A Spring singleton restricts instantiation to one instance per BeanDefinition within an ApplicationContext.

Q02Why does Spring never invoke @PreDestroy lifecycle callbacks on prototype-scoped beans, and how can resource leaks be avoided?Show answer

The container does not retain references to prototype instances after handing them to callers. Because Spring does not track them, it cannot execute @PreDestroy, leaving resource destruction entirely to the caller.

Q03Why does directly injecting a request-scoped bean into a singleton service require a Scoped Proxy?Show answer

A singleton service is created once at application startup when no HTTP request exists. A CGLIB Scoped Proxy delegates method calls to the current thread's RequestAttributes on demand.

Production Pitfalls

Critical Anti-Patterns

  • Pitfall: Directly injecting a prototype into a singleton — The prototype is injected only once at singleton creation time, causing it to freeze and act as an accidental singleton.
  • Pitfall: Allocating unmanaged resources in prototype beans — Spring never invokes @PreDestroy on prototypes, causing file descriptor, socket, or thread leaks if callers do not manually close them.

Primary Documentation & Specifications

Supplementary Module Resources