How do you implement and register a custom Spring scope, and what contract must the Scope interface satisfy?
answer
- implements Scope: get/remove/registerDestructionCallback/resolveContextualObject/getConversationId
- get(name, ObjectFactory) = cache-or-create
- register via ConfigurableBeanFactory.registerScope or CustomScopeConfigurer
- must register in a BeanFactoryPostProcessor (early)
- SimpleThreadScope reference impl; ThreadLocal leaks
basics
~10 sImplement org.springframework.beans.factory.config.Scope (get, remove, registerDestructionCallback, resolveContextualObject, getConversationId), then register it via ConfigurableBeanFactory.registerScope(name, scope) — usually in a BeanFactoryPostProcessor — and use @Scope("myScope") on beans.
solid answer
~40 sA custom scope is a class implementing org.springframework.beans.factory.config.Scope. Its key methods: get(name, ObjectFactory) returns the existing instance for the current context or creates one via the factory and caches it; remove(name) evicts and returns it; registerDestructionCallback(name, runnable) stores a callback to run when the scoped object is discarded; getConversationId() identifies the current scope instance; resolveContextualObject(key) exposes contextual objects like 'request'. You register it with ConfigurableBeanFactory.registerScope("thread", new SimpleThreadScope()) — Spring even ships SimpleThreadScope as an example. Do this in a BeanFactoryPostProcessor (or via CustomScopeConfigurer, which is a ready-made BFPP). Then annotate beans @Scope("thread"). Because the container calls your get on each lookup, you fully control instance lifetime; combine with a scoped proxy to inject into singletons.
code
java · 25 linespublic class TenantScope implements Scope {
private final ThreadLocal<Map<String, Object>> beans =
ThreadLocal.withInitial(HashMap::new);
@Override public Object get(String name, ObjectFactory<?> factory) {
return beans.get().computeIfAbsent(name, n -> factory.getObject());
}
@Override public Object remove(String name) { return beans.get().remove(name); }
@Override public void registerDestructionCallback(String name, Runnable cb) { /* store & run on tenant end */ }
@Override public Object resolveContextualObject(String key) { return null; }
@Override public String getConversationId() { return Thread.currentThread().getName(); }
}
@Configuration
public class ScopeConfig {
@Bean public static CustomScopeConfigurer scopes() {
CustomScopeConfigurer c = new CustomScopeConfigurer();
c.addScope("tenant", new TenantScope());
return c;
}
}
@Component
@Scope(value = "tenant", proxyMode = ScopedProxyMode.TARGET_CLASS)
class TenantContext { /* ... */ }go deeper
Awareness that scopes are pluggable; not expected to implement.
Know the Scope interface exists and registration via CustomScopeConfigurer.
Explain the five methods, get()'s cache-or-create, and BFPP registration timing.
Design a real custom scope (tenant/conversation), handle destruction/leaks/concurrency, and justify vs built-ins.
## The Scope SPI Spring's scope mechanism is extensible via `org.springframework.beans.factory.config.Scope`. You implement it to define a brand-new lifetime rule (a thread, a tenant, a batch step, a conversation, etc.). The container delegates every scoped-bean lookup to your implementation. ### Interface contract ```java public interface Scope { Object get(String name, ObjectFactory<?> objectFactory); Object remove(String name); void registerDestructionCallback(String name, Runnable callback); Object resolveContextualObject(String key); String getConversationId(); } ``` - **get(name, objectFactory)**: return the cached instance for the *current* scope context if present; otherwise call `objectFactory.getObject()` to create it, cache it, and return it. This is the heart of the scope. - **remove(name)**: remove and return the instance from the current context (used when the scope ends). You are responsible for firing any registered destruction callbacks when appropriate. - **registerDestructionCallback(name, Runnable)**: store the callback; run it when the scoped object is destroyed (e.g., when the thread/tenant/conversation ends). Prototype-like scopes that can't track destruction may simply ignore it. - **resolveContextualObject(key)**: return a context object for a key (e.g., `"request"`, `"session"`), or null — used for SpEL/contextual access. Often returns null for simple scopes. - **getConversationId()**: a String id for the current scope instance (e.g., the thread name or session id), or null. ### Reference implementation Spring ships `org.springframework.context.support.SimpleThreadScope` — a `ThreadLocal`-backed scope you can register as an example (note: it does **not** run destruction callbacks because ThreadLocals aren't reliably notified on thread end). ## Registration Two ways: 1. **Programmatically** in a `BeanFactoryPostProcessor`: ```java @Component public class ScopeRegistrar implements BeanFactoryPostProcessor { public void postProcessBeanFactory(ConfigurableListableBeanFactory bf) { bf.registerScope("thread", new SimpleThreadScope()); } } ``` 2. **Declaratively** with `CustomScopeConfigurer` (itself a BeanFactoryPostProcessor): ```java @Bean public static CustomScopeConfigurer scopeConfigurer() { CustomScopeConfigurer c = new CustomScopeConfigurer(); c.addScope("thread", new SimpleThreadScope()); return c; } ``` Registration must happen **before** any bean of that scope is instantiated — hence a BFPP (which runs early). Note the standard scope names `singleton`/`prototype` are reserved and cannot be overridden; `request`/`session`/`application` are pre-registered by the web context. ## Using the scope ```java @Component @Scope(value = "thread", proxyMode = ScopedProxyMode.TARGET_CLASS) public class ThreadLocalData { ... } ``` The `proxyMode` is again needed to inject the thread-scoped bean into a singleton. ## Gotchas & design notes - **Destruction**: if your context (e.g., raw ThreadLocal) can't detect end-of-life, callbacks won't run; document this or use pooled/managed threads with explicit cleanup. - **Memory leaks**: ThreadLocal-based scopes on thread pools retain state across reused threads — clear on task completion. - **Thread-safety**: `get`/`remove` may be called concurrently; guard your backing store. - **When to build one**: rarely — prefer the built-ins. Justified for tenant scope, unit-of-work/conversation scope, or batch-step scope (Spring Batch's `step` scope is itself a custom Scope).
- Why must a custom scope be registered from a BeanFactoryPostProcessor rather than a regular @Bean method alone?Scopes must exist before any bean using them is instantiated. BeanFactoryPostProcessors (and CustomScopeConfigurer, which is one) run early, before singleton pre-instantiation, so the scope name is available when scoped beans are created.
- What is a common leak with a ThreadLocal-backed custom scope on a thread pool?Pooled threads are reused, so state stored in a ThreadLocal survives across unrelated tasks and destruction callbacks may never fire. You must explicitly clear the scope (remove/reset) at the end of each unit of work.
saying these in an interview costs you the question
- Claiming you can override the singleton or prototype scope names
- Registering the scope too late (after scoped beans are created)
- Assuming registerDestructionCallback always runs — ThreadLocal scopes often can't guarantee it
- Inventing a @CustomScope annotation (registration is via registerScope/CustomScopeConfigurer)