Hook: Bean scopes are Spring’s way of answering a simple but important question: who owns this object, and for how long? Interviewers love this because it reveals whether you understand both dependency injection and object lifecycle.
Question: Explain Bean Scopes.
Answer: A bean scope tells Spring how many instances of a bean to create and how long to keep them. The default is singleton, which means one shared instance per Spring application context. Other common scopes are prototype for a new instance on each lookup, and web scopes like request and session for per-request or per-user state.
Interview-Ready Answer: In Spring Boot, a bean scope defines the lifecycle and visibility of a bean. By default, beans are singleton, so I get one shared instance per application context. If I need a fresh object each time, I use prototype; and in web apps, I can use request or session scope for request-specific or user-specific state. One important detail is that Spring manages singleton destruction automatically, but prototype beans are created on demand and are not fully tracked for cleanup.
A scope is the rule Spring uses to decide whether to reuse an object or create a new one. Think of the application context as a factory plus a registry: the scope tells Spring whether to cache the result or hand out fresh copies. In modern Spring Boot, the default is still singleton in both Boot 2 and Boot 3.
| Scope | Instances | Lifetime | Best for |
|---|---|---|---|
| singleton | One per context | Whole app | Stateless services |
| prototype | New on lookup | Caller owns it | Short-lived state |
| request | One per HTTP request | Request duration | Request data |
| session | One per user session | Session duration | Cart, wizard state |
Use singleton for most services, repositories, utilities, and clients because it is simple and memory-efficient. Use prototype only when you truly need a fresh object every time, usually with mutable state. Use request or session scope only in web applications, and prefer them for user-specific state rather than putting that state into a singleton.
The tricky part is not the scope annotation itself, but how you inject the bean. If you inject a prototype bean directly into a singleton, the singleton gets one prototype instance at creation time and then keeps it forever. That surprises many candidates because they expect a new prototype every method call. If you need a fresh prototype inside a singleton, use ObjectProvider, Provider, or method lookup so Spring can fetch a new instance at the moment you need it.
For request and session scope, Spring usually creates a proxy, which is a small wrapper that looks like the bean but resolves the real object only when there is an active request or session. Without a proxy or provider, a singleton trying to use a request-scoped bean can fail because there is no active web request during startup.
From a practical point of view, singleton lookup is cheap after the first creation, while prototype creation pays the full cost each time: object allocation, dependency wiring, and any initialization logic. If a prototype bean graph is large, repeated creation can become expensive under load. The real risk, though, is usually correctness rather than speed: shared mutable singletons can cause data leaks across users, and misused prototypes can create hidden stale state.
Two important gotchas interviewers like to probe:
Real-World Story: Imagine an e-commerce checkout service. The team uses Spring Boot for the pricing engine, tax calculator, and cart flow. The pricing service stays singleton because it is stateless, but the cart state should be per user session. A developer accidentally makes the cart holder a singleton because it looks like a simple helper bean.
Under load, user A adds a laptop, user B adds headphones, and both requests hit the same Java object. Now totals change unpredictably, coupons appear on the wrong account, and support tickets start saying, "My cart changed by itself." In logs, you may see the same bean identity or hash-like value repeated across requests, and the checkout API may return inconsistent totals even though the database is fine.
The outage symptom is subtle at first: items seem to flicker in the UI, totals are wrong only under concurrency, and tests pass locally because one user at a time does not expose the shared-state bug. The fix is to keep the service singleton and stateless, move user data into session scope or the HTTP layer, and avoid storing mutable user-specific fields in shared beans.
package com.example.beanscopes;
import jakarta.annotation.PreDestroy;
import java.util.UUID;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
@SpringBootApplication
public class BeanScopeDemoApplication implements CommandLineRunner {
private final ApplicationContext context;
private final SingletonConsumer singletonConsumer;
public BeanScopeDemoApplication(ApplicationContext context, SingletonConsumer singletonConsumer) {
this.context = context;
this.singletonConsumer = singletonConsumer;
}
public static void main(String[] args) {
SpringApplication.run(BeanScopeDemoApplication.class, args);
}
@Override
public void run(String... args) {
System.out.println("=== Direct lookups from the container ===");
SingletonCounter s1 = context.getBean(SingletonCounter.class);
SingletonCounter s2 = context.getBean(SingletonCounter.class);
System.out.println("singleton same instance? " + (s1 == s2));
System.out.println("singleton id A: " + s1.getId());
System.out.println("singleton id B: " + s2.getId());
PrototypeCounter p1 = context.getBean(PrototypeCounter.class);
PrototypeCounter p2 = context.getBean(PrototypeCounter.class);
System.out.println("prototype same instance? " + (p1 == p2));
System.out.println("prototype id A: " + p1.getId());
System.out.println("prototype id B: " + p2.getId());
System.out.println("\n=== Prototype injected into a singleton ===");
singletonConsumer.printInjectedPrototype();
System.out.println("\n=== Prototype resolved lazily with ObjectProvider ===");
singletonConsumer.printProviderPrototype();
System.out.println("\n=== Edge case ===");
System.out.println("Spring creates prototype beans on demand, but it does NOT call their @PreDestroy methods automatically when the context closes.");
System.out.println("So if a prototype holds resources, you must clean them up yourself.");
}
@Bean
public SingletonCounter singletonCounter() {
return new SingletonCounter();
}
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public PrototypeCounter prototypeCounter() {
return new PrototypeCounter();
}
@Bean
public SingletonConsumer singletonConsumer(PrototypeCounter prototypeCounter,
ObjectProvider<PrototypeCounter> prototypeProvider) {
return new SingletonConsumer(prototypeCounter, prototypeProvider);
}
static final class SingletonCounter {
private final UUID id = UUID.randomUUID();
UUID getId() {
return id;
}
@PreDestroy
public void cleanup() {
System.out.println("Destroying singleton " + id);
}
}
static final class PrototypeCounter {
private final UUID id = UUID.randomUUID();
UUID getId() {
return id;
}
@PreDestroy
public void cleanup() {
System.out.println("Destroying prototype " + id + " (this line will usually not appear)");
}
}
static final class SingletonConsumer {
private final PrototypeCounter injectedPrototype;
private final ObjectProvider<PrototypeCounter> prototypeProvider;
SingletonConsumer(PrototypeCounter injectedPrototype, ObjectProvider<PrototypeCounter> prototypeProvider) {
this.injectedPrototype = injectedPrototype;
this.prototypeProvider = prototypeProvider;
}
void printInjectedPrototype() {
System.out.println("Injected prototype id: " + injectedPrototype.getId());
System.out.println("Same object every time inside this singleton: " + injectedPrototype.getId());
}
void printProviderPrototype() {
PrototypeCounter a = prototypeProvider.getObject();
PrototypeCounter b = prototypeProvider.getObject();
System.out.println("provider A id: " + a.getId());
System.out.println("provider B id: " + b.getId());
System.out.println("provider A == provider B ? " + (a == b));
}
}
}
Follow-up & Tricky Questions:
ObjectProvider, Provider, or lookup methods so the singleton asks Spring for a new instance only when it needs one.@PreDestroy is not called automatically for them.ServletContext, so it is shared by the entire web application, not by one user.Common Mistakes:
ObjectProvider or a proxy for lazy retrieval.Memory Hook: Remember this picture: singleton = one house key for everyone, prototype = a new key every time you ask, request = a key that works for one visit, session = a key that lasts for one guest stay.
Cheat Sheet:
singleton.singleton means one instance per application context.prototype means a new instance on each lookup.Practice Tasks:
ObjectProvider.