🍃 Section 5 · Question #12

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:

  1. ObjectProvider<T>.getObject(): Explicit, type-safe on-demand resolution.
  2. @Lookup Method Injection: Spring overrides an abstract getter method using CGLIB to query BeanFactory.getBean() dynamically.
  3. 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:

  1. 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.
  2. Prototype Cost:
    • On every single invocation, the container executes:
      1. Constructor reflection (Constructor.newInstance()).
      2. Annotation scanning and dependency injection via AutowiredAnnotationBeanPostProcessor.
      3. Execution of @PostConstruct and Aware callbacks.
      4. Dynamic proxy generation via ProxyFactory (if annotated with aspects).
    • This churns through millions of transient heap allocations per second, triggering frequent Young Generation GC pauses and CPU memory bus saturation.

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 Java new allows 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 @Lookup method is declared abstract, 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:

  1. Resolves constructor candidates.
  2. Instantiates via reflection (Constructor.newInstance()).
  3. Runs the complete BeanPostProcessor chain (injecting @Autowired fields, evaluating @Value).
  4. Executes @PostConstruct and Aware callbacks.
  5. 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

“singleton and prototype are Spring’s core non-web scopes:

  • singleton (default): Exactly 1 instance per ApplicationContext, cached in singletonObjects, 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 BeanCurrentlyInCreationException crashes for prototypes.”

Red Flags & Pitfalls (DO NOT Say)

  • ❌ “Spring calls @PreDestroy on 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 │
└───────────────────────┴──────────────────────────────┴──────────────────────────────┘