Why is a @PreDestroy method on a prototype-scoped bean never called, and what can you do about it?
answer
- Container manages init, not full lifecycle, of prototypes
- No reference kept → nothing to call destroy on
- Init runs, destroy doesn't = asymmetric
- Fix: explicit cleanup / ObjectProvider / custom BPP
- request/session scopes DO destroy
basics
~20 sSpring 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 sFor 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 linesimport 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
Know that prototypes don't get destroy callbacks; cleanup is manual.
Explain that the container stops tracking after handoff and give one remediation.
Contrast with request/session scope, and discuss ObjectProvider/@Lookup for true per-call instances.
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