skip to content

Why is a @PreDestroy method on a prototype-scoped bean never called, and what can you do about it?

level: seniorimportance: should knowfreq 45%

answer

  1. Container manages init, not full lifecycle, of prototypes
  2. No reference kept → nothing to call destroy on
  3. Init runs, destroy doesn't = asymmetric
  4. Fix: explicit cleanup / ObjectProvider / custom BPP
  5. request/session scopes DO destroy

basics

~20 s

Spring doesn't manage a prototype's full lifecycle — it creates and hands off the instance without tracking it, so no destroy callback runs. To clean up, do it manually or use a custom BeanPostProcessor/DisposableBeanAdapter yourself.

solid answer

~40 s

For a prototype-scoped bean the container instantiates it, injects dependencies, and runs the initialization callbacks — but then hands the instance to the caller and forgets about it. Unlike singletons, prototypes aren't retained in the container's destruction registry, so @PreDestroy, DisposableBean.destroy(), and the configured destroyMethod are never invoked; the client is responsible for teardown. Practical options: (1) call cleanup explicitly in client code; (2) inject an ObjectProvider/factory and wrap acquisition in try-finally; (3) register a custom BeanPostProcessor that holds references and drives destruction, or wrap the prototype in a DisposableBeanAdapter and call it yourself; (4) reconsider scope — often what you want is a singleton or a well-scoped resource. This asymmetry (init runs, destroy doesn't) is a classic source of resource leaks with prototype-scoped pooled or connection-like beans.

code

java · 20 lines
java
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Component;

@Component
class Worker {
    private final ObjectProvider<Task> taskProvider; // Task is @Scope("prototype")

    Worker(ObjectProvider<Task> taskProvider) {
        this.taskProvider = taskProvider;
    }

    void run() {
        Task task = taskProvider.getObject(); // fresh prototype
        try {
            task.execute();
        } finally {
            task.cleanup(); // must clean up manually — Spring won't call @PreDestroy
        }
    }
}

go deeper

for a junior

Know that prototypes don't get destroy callbacks; cleanup is manual.

for a middle

Explain that the container stops tracking after handoff and give one remediation.

for a senior

Contrast with request/session scope, and discuss ObjectProvider/@Lookup for true per-call instances.

for a principal

Discuss the DefaultSingletonBeanRegistry disposable tracking, DisposableBeanAdapter, and when a prototype-for-resource is a design smell.

**Bean scope** controls how many instances the container creates and how long it keeps them. A **singleton** (the default) is created once and the container keeps a reference for the whole context lifetime. A **prototype** is created fresh on every injection/`getBean` call. **The core reason destroy callbacks don't fire for prototypes:** Spring's contract, stated in the reference docs, is that the container **does not manage the complete lifecycle of a prototype bean**. It runs *configuration and initialization* — constructor, injection, aware-callbacks, `@PostConstruct`, `afterPropertiesSet()`, the init method — and then **releases the instance to the client and stops tracking it**. Because the container holds no reference, it has nothing to invoke `@PreDestroy` / `DisposableBean.destroy()` / `destroyMethod` on when the context shuts down. (For singletons, the container keeps them in a `DefaultSingletonBeanRegistry` with `disposableBeans`, which is what drives orderly shutdown.) Analogy: the container is like a factory that assembles and starts a machine, then ships it — it no longer knows where the machine went, so it can't send a technician to switch it off. **Consequences / gotcha:** This is asymmetric — *init runs but destroy doesn't* — which surprises people who put resource-opening logic in `@PostConstruct` of a prototype and expect symmetric cleanup in `@PreDestroy`. That leaks file handles, sockets, threads, etc. **Remedies:** 1. **Explicit cleanup by the client.** The client that obtained the prototype calls a `close()`/cleanup method in a `try-finally`. Simplest and most honest. 2. **`ObjectProvider` / `@Lookup` / factory injection.** Instead of injecting the prototype directly into a singleton (which would freeze a single instance), inject `ObjectProvider<T>` and get a fresh instance per use, cleaning it up in `finally`. 3. **Custom `BeanPostProcessor`.** Register a post-processor that, in `postProcessBeforeDestruction` or by keeping its own registry, drives teardown. You can build a `DisposableBeanAdapter` around the prototype and invoke it yourself. 4. **Rethink the scope.** Often a 'prototype for a resource' is a design smell; a singleton holding a pool, or a request/session-scoped bean (whose destruction *is* managed by the web scope), fits better. Note: request/session/websocket scopes **do** honor destruction callbacks because those scopes track instances and destroy them at scope end. **Related nuance — singleton depending on prototype:** If a singleton injects a prototype directly, the prototype is resolved once at singleton-creation time and effectively becomes a singleton in practice; its (non-existent) destruction still won't be managed. Use method injection (`@Lookup`) or `ObjectProvider` for true per-call prototypes. **Interview framing:** The strongest answer names the exact reason (container stops tracking after handing off), states the three affected hooks, contrasts with request/session scope (which *do* destroy), and gives a concrete remediation like `ObjectProvider` + `try-finally`.

  • Do request- or session-scoped beans get their destroy callbacks called?
    Yes. Those scopes track their instances and invoke destruction callbacks when the request/session ends, unlike prototype scope.
  • What happens to a prototype if a singleton injects it directly by field?
    It's resolved once at the singleton's creation and behaves like a single shared instance; you won't get a new one per use. Use @Lookup or ObjectProvider for true per-call semantics.

saying these in an interview costs you the question

  • Assuming destroy callbacks fire for prototypes just like singletons
  • Saying Spring uses the garbage collector to trigger @PreDestroy
  • Not knowing that request/session scopes DO honor destruction

context