skip to content

How do you implement and register a custom Spring scope, and what contract must the Scope interface satisfy?

level: principalimportance: nice to knowfreq 25%

answer

  1. implements Scope: get/remove/registerDestructionCallback/resolveContextualObject/getConversationId
  2. get(name, ObjectFactory) = cache-or-create
  3. register via ConfigurableBeanFactory.registerScope or CustomScopeConfigurer
  4. must register in a BeanFactoryPostProcessor (early)
  5. SimpleThreadScope reference impl; ThreadLocal leaks

basics

~10 s

Implement 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 s

A 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 lines
java
public 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

for a junior

Awareness that scopes are pluggable; not expected to implement.

for a middle

Know the Scope interface exists and registration via CustomScopeConfigurer.

for a senior

Explain the five methods, get()'s cache-or-create, and BFPP registration timing.

for a principal

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)

context