Difference between singleton and prototype scope
The lifecycles of singleton and prototype beans align during the creation phase, but diverge completely at the destruction phase:
🟢 Junior Level
1. Core Differences at a Glance
singleton and prototype are the two foundational, universal scopes in the Spring Framework. They define fundamentally different instance counts, creation timings, and lifecycle management models:
| Dimension | singleton Scope (Default) |
prototype Scope |
|---|---|---|
| Instance Cardinality | Exactly 1 instance per ApplicationContext |
A brand-new instance on every request or injection |
| Instantiation Timing | Eagerly at container startup (unless @Lazy) |
On demand at runtime when requested |
| Container Caching | Cached permanently in singletonObjects |
Not cached by Spring after creation |
@PostConstruct |
Executed once at startup | Executed on every new instance |
@PreDestroy |
✅ Executed during context shutdown | ❌ NEVER EXECUTED BY SPRING! |
| Statefulness | Must be Stateless (Thread-safe) | Can be Stateful (Isolated per caller) |
2. Basic Code Example
package com.example.demo;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
// 1. Singleton: Shared across all threads and user requests
@Component
public class CacheManager {
// Stateless shared service
}
// 2. Prototype: Independent object allocated on each request
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class DataImportJob {
private String sourceFile; // Encapsulates job-specific mutable state
public void setSourceFile(String sourceFile) {
this.sourceFile = sourceFile;
}
}
// Testing instance identity in code:
CacheManager c1 = context.getBean(CacheManager.class);
CacheManager c2 = context.getBean(CacheManager.class);
System.out.println(c1 == c2); // true: Points to the exact same memory address in heap
DataImportJob j1 = context.getBean(DataImportJob.class);
DataImportJob j2 = context.getBean(DataImportJob.class);
System.out.println(j1 == j2); // false: Two completely distinct objects in heap
🟡 Middle Level
1. Lifecycle Divergence
The lifecycles of singleton and prototype beans align during the creation phase, but diverge completely at the destruction phase:
Lifecycle Phase Singleton Bean Prototype Bean
────────────────────────────────────────────────────────────────────────
1. Instantiation Executed once Executed N times
2. Populate Properties Executed once Executed N times
3. Aware Interfaces Executed once Executed N times
4. BPP BeforeInit Executed once Executed N times
5. @PostConstruct Executed once Executed N times
6. BPP AfterInit (Proxy) Executed once Executed N times
────────────────────────────────────────────────────────────────────────
7. Retained in Cache Stored in container Handed to caller & untracked
────────────────────────────────────────────────────────────────────────
8. @PreDestroy ✅ Invoked on shutdown ❌ NEVER INVOKED BY CONTAINER
9. DisposableBean ✅ Invoked on shutdown ❌ NEVER INVOKED BY CONTAINER
10. Custom destroyMethod ✅ Invoked on shutdown ❌ NEVER INVOKED BY CONTAINER
2. The Prototype Resource Cleanup Dilemma
Because Spring does not manage destruction callbacks for prototype beans, any acquired external resources (e.g., file descriptors, sockets, database transactions) will leak unless cleaned up explicitly by the client:
@Service
public class ReportOrchestrator {
private final ObjectProvider<PdfExportTask> taskProvider;
public ReportOrchestrator(ObjectProvider<PdfExportTask> taskProvider) {
this.taskProvider = taskProvider;
}
public void generateReport() {
PdfExportTask task = taskProvider.getObject();
try {
task.execute();
} finally {
// Mandatory manual cleanup: Spring will not call @PreDestroy!
task.cleanup();
}
}
}
3. The Prototype-in-Singleton Injection Hazard (Scoped Mismatch)
Directly injecting a prototype into a singleton is a classic interview trap:
@Service // Singleton
public class OrderService {
@Autowired // ❌ DANGEROUS: Injected ONLY ONCE at startup!
private PaymentValidator validator; // Prototype bean
}
Consequence: The prototype bean is resolved once during the singleton’s initialization and remains frozen inside the singleton field. Every subsequent call to OrderService reuses the exact same prototype instance.
Solutions:
ObjectProvider<T>.getObject(): Explicit, type-safe on-demand resolution.@LookupMethod Injection: Spring overrides an abstract getter method using CGLIB to queryBeanFactory.getBean()dynamically.- Scoped Proxy (
ScopedProxyMode.TARGET_CLASS): Spring injects a dynamic proxy that delegates each method call to a fresh instance.
🔴 Senior Level
1. Circular Dependency Asymmetry
The behavioral difference between scopes when resolving circular dependencies ($A \leftrightarrow B$) is absolute:
Singleton Circular Dependency:
[Bean A] <─────────────────> [Bean B]
-> Resolved safely via DefaultSingletonBeanRegistry 3-level cache (singletonFactories)!
Prototype Circular Dependency:
[Proto A] <────────────────> [Proto B]
-> Enters infinite recursive allocation loop!
-> Container crashes at startup with BeanCurrentlyInCreationException!
Why Prototype Circular Dependencies Cannot Be Resolved:
In singletons, Spring publishes an uninitialized early instance to the 3rd level cache (singletonFactories). When Bean $B$ needs Bean $A$, it retrieves the uninitialized instance, allowing the cycle to close.
For prototype beans, early instance exposure is impossible because prototypes are not tracked or stored in the container’s registry. Requesting ProtoA instantiates a new ProtoA, which requires ProtoB, which in turn requests a brand-new ProtoA, causing an unbounded recursive loop until Spring detects the recursion and throws BeanCurrentlyInCreationException.
2. High-Throughput Hotpaths & GC Allocation Pressure
Using Prototype beans inside high-throughput transaction paths (e.g., servicing 50,000 requests/sec) can severely degrade application throughput:
- Singleton Cost:
- Reflection, annotation scanning, and proxy generation happen once at startup.
- At runtime, invoking methods on singletons is a direct JVM pointer invocation capable of full JIT compilation and inlining.
- Prototype Cost:
- On every single invocation, the container executes:
- Constructor reflection (
Constructor.newInstance()). - Annotation scanning and dependency injection via
AutowiredAnnotationBeanPostProcessor. - Execution of
@PostConstructand Aware callbacks. - Dynamic proxy generation via
ProxyFactory(if annotated with aspects).
- Constructor reflection (
- This churns through millions of transient heap allocations per second, triggering frequent Young Generation GC pauses and CPU memory bus saturation.
- On every single invocation, the container executes:
3. Decision Framework: Prototype vs Plain Java new
Is a fresh object instance required?
│
├── NO ──► Use Singleton (Stateless)
│
└── YES ─► Does the object need Spring features (@Autowired, @Transactional, @Value)?
│
├── NO ──► Use plain Java: `new MyTask()`!
│ (100x faster, zero container overhead, enables Escape Analysis)
│
└── YES ──► Use Prototype via ObjectProvider<T>
[!TIP] Senior Architect Rule: If a class does not require injected Spring collaborators or AOP proxies, declaring it as
@Scope("prototype")is an anti-pattern. Instantiating via plain Javanewallows the HotSpot C2 compiler to perform Escape Analysis and Scalar Replacement, allocating fields directly in CPU registers or on the thread stack without touching the heap.
4 Tricky Questions
1. Why does Spring intentionally omit calling @PreDestroy and DisposableBean.destroy() on prototype-scoped beans?
Answer: Spring does not retain references to prototype beans after they are initialized and delivered to the caller.
If Spring retained references to every prototype instance to manage their destruction upon context shutdown, the container would create an enormous, un-garbage-collectable memory leak throughout the application’s runtime. The container relinquishes complete ownership of prototype beans upon delivery; therefore, resource cleanup and lifecycle termination are strictly the responsibility of the client code holding the reference.
2. What happens if two prototype-scoped beans have a circular dependency on each other?
Answer:
The application immediately crashes with a BeanCurrentlyInCreationException.
Spring’s 3-level singleton cache (singletonFactories) only resolves circular references for singleton beans by exposing early references. Prototypes are not cached or published to early-exposure registries. When ProtoA is requested, Spring instantiates it and attempts to populate its dependency ProtoB. Instantiating ProtoB requires a new ProtoA, which in turn triggers another ProtoB. Spring’s AbstractBeanFactory detects that the requested bean is already flagged as “currently in creation” on the current thread and throws BeanCurrentlyInCreationException: Is there an unresolvable circular reference?.
3. How does @Lookup method injection work under the hood without injecting the ApplicationContext?
Answer: Spring uses CGLIB dynamic bytecode generation at startup to subclass the bean:
- If the containing class or the
@Lookupmethod is declaredabstract, Spring provides a concrete subclass implementation. - If the method is concrete, Spring overrides it.
- The generated bytecode intercepts calls to the method and translates them directly into a call to the underlying
BeanFactory:// Conceptual bytecode generated by Spring CGLIB: public MyPrototype getMyPrototype() { return this.beanFactory.getBean(MyPrototype.class); }This achieves dynamic on-demand retrieval while keeping domain classes free of Spring container imports (avoiding the Service Locator anti-pattern).
4. Why is creating short-lived data objects via @Scope("prototype") instead of plain Java new considered an antipattern in high-throughput systems?
Answer: Requesting a prototype bean from Spring invokes the container’s entire creation pipeline on every request:
- Resolves constructor candidates.
- Instantiates via reflection (
Constructor.newInstance()). - Runs the complete
BeanPostProcessorchain (injecting@Autowiredfields, evaluating@Value). - Executes
@PostConstructand Aware callbacks. - Potentially creates a dynamic AOP proxy wrapper (
ProxyFactory).
In high-throughput systems, this causes severe CPU overhead and floods the Young generation with short-lived metadata objects, triggering frequent GC pauses. Plain Java new executes in single-digit nanoseconds and enables the HotSpot C2 compiler to perform Escape Analysis (allocating the object on the stack or eliminating it via scalar replacement).
🎯 Interview Cheat Sheet
30-Second Elevator Pitch
“
singletonandprototypeare Spring’s core non-web scopes:
singleton(default): Exactly 1 instance perApplicationContext, cached insingletonObjects, must be stateless, and Spring manages its complete lifecycle including@PreDestroy.prototype: A new instance is created on every call, container does not cache it, and Spring never calls@PreDestroy.- Injected prototypes in singletons freeze unless wired via
ObjectProvider<T>,@Lookup, or a Scoped Proxy.- Circular dependencies are resolvable for singletons (via the 3-level cache), but cause immediate
BeanCurrentlyInCreationExceptioncrashes for prototypes.”
Red Flags & Pitfalls (DO NOT Say)
- ❌ “Spring calls
@PreDestroyon prototype beans right before they are garbage collected.” (Spring never tracks prototype destruction; the GC knows nothing about Spring lifecycle annotations). - ❌ “Circular dependencies between prototype beans can be resolved with
@Lazy.” (Direct mutual prototype cycles are unresolvable). - ❌ “Prototype beans are faster than singletons because they don’t have locking contention.” (Creating a prototype triggers heavy reflection, BPP, and proxy overhead on every call).
- ❌ “Singleton beans are inherently thread-safe in Spring.” (Spring singletons are shared across all worker threads; they are only thread-safe if designed as stateless POJOs).
Singleton vs Prototype Comparison Matrix
Singleton vs Prototype Deep Dive:
┌───────────────────────┬──────────────────────────────┬──────────────────────────────┐
│ Criterion │ Singleton Scope │ Prototype Scope │
├───────────────────────┼──────────────────────────────┼──────────────────────────────┤
│ Instance Count │ Exactly 1 per context │ N instances (On demand) │
│ Creation Moment │ Eager at container boot │ Lazy upon getBean()/inject │
│ Stored in Cache? │ Yes (singletonObjects) │ No (Discarded after init) │
│ @PostConstruct │ Invoked 1 time │ Invoked N times │
│ @PreDestroy │ ✅ Invoked on context.close()│ ❌ NEVER INVOKED │
│ Circular Dependencies │ Resolved via 3-level cache │ Fails (BeanCurrentlyInCreation)
│ Concurrency Model │ Must be Stateless │ Can be Stateful │
│ Clean Creation Option │ Framework Standard │ Prefer `new` if no DI needed │
└───────────────────────┴──────────────────────────────┴──────────────────────────────┘