Understanding Spring Bean Scopes: Singleton, Prototype, Request & Session

A comprehensive guide to Spring IoC bean Scopes with real-world examples, code walkthroughs, and gotchas you must know.
1. What Are Bean Scopes
In Spring, the scope of a bean defines its lifecycle - how long a bean instance lives and how many instances of that bean exist at any point in time.
Spring provides four primary scopes:
Scope
/ \
Singleton Request
| \
Prototype Session
For Singleton scope 1 per IOC container instances are created and Initialized eagerly at startup.
For Prototype scope one new instances are created and initialization is lazy.
For Request scope 1 per HTTP request instances are created and initialization is lazy.
For Session scope 1 per HTTP session instances are created and initialization is lazy.
2. Singleton Scope
Singleton is the default scope in Spring. Only one instance of the bean is created per IoC (Inversion of Control) container, and its eagerly initialized - meaning the object is created when the application starts up, not when it's first requested.
Key Properties:
Default scope - no annotation needed, but you can use @Scope("singleton")
Only 1 instance per IOC container
Eagerly initialized at application startup
The same object is shared across all components that inject it
Code Example
TestController1.java
@RestController
public class TestController1{
@Autowired
User user;
public TestController1(){
System.out.println("TestController1 instance initialization");
}
@PostConstruct
public void init(){
System.out.println("TestController1 object hashCode: " + this.hashCode());
}
@GetMapping(path = "/fetchUser")
public ResponseEntity<String> getUserDetails(){
System.out.println("fetchUser api invoked");
return ResponseEntity.status(HttpStatus.OK).body("");
}
}
User.java (Singleton Bean)
@Component
public class User{
public User(){
System.out.println("User initialization");
}
@PostConstruct
public void init(){
System.out.println("User object hashCode: " + this.hashCode());
}
}
Console Output:
TestController1 instance initialization
User initialization
User object hashCode: 1140202235
TestController1 object hashCode: 1046302571 User object hashCode: 1140202235
TestController2 instance initialization
TestController2 object hashCode: 1525241607 User object hashCode: 1140202235
Notice something important here: both TestController1 and TestController2 print the same User object hashCode (1140402235). This confirms that only one instance of User exists across the entire application - the essence of Singleton scope.
3. Prototype Scope
In Prototype scope, a new object is created every single time the bean is requested from the container. Unlike Singleton, it is lazily initialized - the object is only created when it is actually needed.
Key Properties
Annotated with @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
New instance created every time the bean is injected or requested
Lazily initialized
Each consumer gets its own private instance
Code Example:
TestController1.java
@RestController
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@RequestMapping(value = "/api/")
public class TestController1 {
@Autowired
User user;
@Autowired
Student student;
public TestController1() {
System.out.println("TestController1 instance initialization");
}
@PostConstruct
public void init() {
System.out.println("TestController1 object hashCode: " + this.hashCode()
+ " User object hashCode: " + user.hashCode()
+ " Student object hashCode: " + student.hashCode());
}
@GetMapping(path = "/fetchUser")
public ResponseEntity<String> getUserDetails() {
System.out.println("fetchUser api invoked");
return ResponseEntity.status(HttpStatus.OK).body("");
}
}
Student.java(Prototype Bean)
@Component
public class Student {
@Autowired
User user;
public Student() {
System.out.println("Student instance initialization");
}
@PostConstruct
public void init() {
System.out.println("Student object hashCode: " + this.hashCode()
+ " User object hashCode: " + user.hashCode());
}
}
User.java(Prototype Bean)
@Component
@Scope("prototype")
public class User {
public User() {
System.out.println("User initialization");
}
@PostConstruct
public void init() {
System.out.println("User object hashCode: " + this.hashCode());
}
}
Console Output on Startup
Student instance initialization
User initialization
User object hashCode: 1510009630
Student object hashCode: 2092450685 User object hashCode: 1510009630
Console Output After HTTP Request to /api/fetchUser
TestController1 instance initialization
User initialization
User object hashCode: 1984730322
TestController1 object hashCode: 1786739207 User object hashCode: 1984730322 Student object hashCode: 2092450685
fetchUser api invoked
Notice Here:
The Student bean was created at startup (because Student is not prototype - just User is in this scenario)
When the HTTP request hits, a brand new User with a different hashCode (1984730322) is created for TestController1
Each request produces a fresh instance
4. Request Scope
In Request scope, a new bean instance is created for every HTTP request. Once the request completes, the bean is discarded. It is lazily initialized - created only when a request comes in.
Key Properties
Annotated with @Scope("request") or @RequestScope
New object per HTTP request
Lazily initialized
Perfect for holding request-specific data
Code Example
TestController1.java
@RestController
@Scope("request")
@RequestMapping(value = "/api/")
public class TestController1 {
@Autowired
User user;
@Autowired
Student student;
public TestController1() {
System.out.println("TestController1 instance initialization");
}
@PostConstruct
public void init() {
System.out.println("TestController1 object hashCode: " + this.hashCode()
+ " User object hashCode: " + user.hashCode()
+ " Student object hashCode: " + student.hashCode());
}
@GetMapping(path = "/fetchUser")
public ResponseEntity<String> getUserDetails() {
System.out.println("fetchUser api invoked");
return ResponseEntity.status(HttpStatus.OK).body("");
}
}
User.java(Request-Scoped Bean)
@Component
@Scope("request")
public class User {
public User() {
System.out.println("User initialization");
}
@PostConstruct
public void init() {
System.out.println("User object hashCode: " + this.hashCode());
}
}
Console Output - First Request
TestController1 instance initialization
User initialization
User object hashCode: 39793904
Student instance initialization
Student object hashCode: 275139209 User object hashCode: 39793904
TestController1 object hashCode: 898967761 User object hashCode: 39793904 Student object hashCode: 275139209
fetchUser api invoked
Console Output - Second Request
TestController1 instance initialization
User initialization
User object hashCode: 1227388929
Student instance initialization
Student object hashCode: 1206886228 User object hashCode: 1227388929
TestController1 object hashCode: 1137709937 User object hashCode: 1227388929 Student object hashCode: 1206886228
fetchUser api invoked
Each HTTP request produces completely new instances of TestController1, User, and Student - with entirely different hashCodes. This proves the request-scoped bean lifecycle is tied strictly to the HTTP request.
5. The Classic Gotcha
A "gotcha" is developer slang for " a mistake that's easy to make and hard to spit until it blows up."
What happens when you inject a Request-scoped bean into a Singleton-scoped bean?
This is one of the most common Spring pitfalls. Consider this scenario:
TestController1.java - Singleton scope
@RestController
@Scope("singleton")
@RequestMapping(value = "/api/")
public class TestController1 {
@Autowired
User user; // User is Request-scoped!
// ...
}
User.java - Request scope
@Component
@Scope("request")
public class User {
// ...
}
What happens?
org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name 'testController1':
Unsatisfied dependency expressed through field 'user'
The application fails to start with an error!
Why does this happen?
Spring created the Request scope bean only when there is an active HTTP request present. Since TestController1 is Singleton, it is eagerly initialized at startup - and at that point, there is no active HTTP request in the current thread. So Spring simply cannot create the User bean, and the dependency injection fails.
Think of it this way:
A Singleton bean lives for the entire application lifetime. A Request-scoped bean only lives during an HTTP request. You can't inject something short-lived into something long-lived directly.
Fixing the Gotcha with Scoped Proxy
Solution: proxyMode = ScopedProxyMode.TARGET_CLASS
Spring provides an elegant solution: Scoped Proxy. Instead of injecting the real User bean, Spring injects a proxy object. When a method is called on this proxy, it delegates to the actual User bean that exists in the current HTTP request context.
User.java - Fixed with Scoped Proxy
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class User {
public User() {
System.out.println("User initialization");
}
@PostConstruct
public void init() {
System.out.println("User object hashCode: " + this.hashCode());
}
public void dummyMethod() {
// placeholder method
}
}
Console Output After Fix
TestController1 instance initialization
TestController1 object hashCode: 1356419559 User object hashCode: 1159352444
At startup, the User hashCode shown is that of the proxy object (not a real User bean yet). Notice "User initialization" is not printed at startup anymore.
After HTTP Request:
fetchUser api invoked
User initialization
User object hashCode: 1078757370
Now "User Initialization" prints after the API is invoked - because the real User bean is created lazily when the HTTP request arrives.
This is the power of ScopedProxyMode.TARGET_CLASS - it allows long-lived beans to safely hold references to short-lived beans.
6. Session Scope
Session scope creates a new bean instance per HTTP session. The session is created when a user first accesses any endpoint and remains active until it expires or is explicitly invalidated.
Key Properties
Annotated with Scope("session")
New object per HTTP session (not per request)
Lazily initialized
Perfect for holding user-specific state across multiple requests
Remains active until session expires or is invalidated
Code Example:
TestController1.java
@RestController
@Scope(value = "session")
@RequestMapping(value = "/api/")
public class TestController1 {
@Autowired
User user;
public TestController1() {
System.out.println("TestController1 instance initialization");
}
@PostConstruct
public void init() {
System.out.println("TestController1 object hashCode: " + this.hashCode()
+ " User object hashCode: " + user.hashCode());
}
@GetMapping(path = "/fetchUser")
public ResponseEntity<String> getUserDetails() {
System.out.println("fetchUser api invoked");
return ResponseEntity.status(HttpStatus.OK).body("");
}
@GetMapping(path = "/logout")
public ResponseEntity<String> getUserDetails(HttpServletRequest request) {
System.out.println("end the session");
HttpSession session = request.getSession();
session.invalidate();
return ResponseEntity.status(HttpStatus.OK).body("");
}
}
User.java(Session Bean)
@Component
public class User {
public User() {
System.out.println("User initialization");
}
@PostConstruct
public void init() {
System.out.println("User object hashCode: " + this.hashCode());
}
public void dummyMethod() {}
}
Lifecycle Demo
Request1 -> /api/fetchUser
User initialization
User object hashCode: 254812629
TestController1 object hashCode: 1476570007 User object hashCode: 254812629
fetchUser api invoked
Request2 -> /api/fetchUser (same session)
TestController1 object hashCode: 1476570007 User object hashCode: 254812629
fetchUser api invoked
Same objects - same session!
Request3 -> /api/logout
end the session
Request4 -> /api/fetchUser (new session after logout)
TestController1 instance initialization
User object hashCode: 254812629
TestController1 object hashCode: cb6839a User object hashCode: 254812629
fetchUser api invoked
New TestController1 instance, but User is Singleton (same hashCode) - fresh session-scoped controller, shared User.
7. Key Takeaways
Singleton is the default — one instance per IoC container, eagerly initialized at startup.
Prototype creates a new instance every time — useful when you need stateful, independent objects.
Request scope creates a fresh bean per HTTP request — ideal for request-specific processing.
Session scope creates a bean per user session — great for maintaining user state across requests.
Never inject a shorter-lived bean directly into a longer-lived bean. For example, injecting a Request-scoped bean into a Singleton will cause an UnsatisfiedDependencyException.
Use proxyMode = ScopedProxyMode.TARGET_CLASS to safely inject shorter-lived (Request/Session) beans into longer-lived (Singleton) beans. The proxy handles the delegation at runtime.
Conclusion
Understanding bean scopes is fundamental to building correct, efficient Spring Boot applications. Getting it wrong can lead to subtle bugs — like sharing state across requests when you shouldn't, or application crashes due to scope mismatch.
The golden rule: match the bean's scope to its actual lifecycle requirements, and always use a scoped proxy when mixing scopes.
Happy coding!




