Backend Development 7 min read

Mastering Spring Bean Scopes: Custom Scope Creation and Prototype Injection

This article explains how Spring's @Scope annotation determines bean lifecycles, demonstrates creating and registering a custom scope, and shows how to correctly inject prototype-scoped beans into singleton components using scoped proxies.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Mastering Spring Bean Scopes: Custom Scope Creation and Prototype Injection

1. Bean Scope in Spring

In Spring 5.3.23, the @Scope annotation defines a bean's lifecycle. By default, beans are singletons ( ConfigurableBeanFactory.SCOPE_SINGLETON ). When a bean is created, Spring checks the bean definition's scope and creates the instance accordingly.

<code>protected <T> T doGetBean(String name, @Nullable Class<T> requiredType,
        @Nullable Object[] args, boolean typeCheckOnly) throws BeansException {
    String beanName = transformedBeanName(name);
    Object bean;
    Object sharedInstance = getSingleton(beanName);
    if (sharedInstance != null && args == null) {
        // use existing singleton
    } else {
        try {
            RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
            checkMergedBeanDefinition(mbd, beanName, args);
            // guarantee initialization of dependent beans
            // create bean instance based on scope
            if (mbd.isSingleton()) {
                sharedInstance = getSingleton(beanName, () -> {
                    try {
                        return createBean(beanName, mbd, args);
                    } catch (BeansException ex) {
                        destroySingleton(beanName);
                        throw ex;
                    }
                });
                bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
            } else if (mbd.isPrototype()) {
                Object prototypeInstance = null;
                try {
                    beforePrototypeCreation(beanName);
                    prototypeInstance = createBean(beanName, mbd, args);
                } finally {
                    afterPrototypeCreation(beanName);
                }
                bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
            } else {
                String scopeName = mbd.getScope();
                if (!StringUtils.hasLength(scopeName)) {
                    throw new IllegalStateException("No scope name defined for bean " + beanName);
                }
                Scope scope = this.scopes.get(scopeName);
                if (scope == null) {
                    throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
                }
                try {
                    Object scopedInstance = scope.get(beanName, () -> {
                        beforePrototypeCreation(beanName);
                        try {
                            return createBean(beanName, mbd, args);
                        } finally {
                            afterPrototypeCreation(beanName);
                        }
                    });
                    bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
                } catch (IllegalStateException ex) {
                    throw new BeanCreationException(beanName,
                        "Scope '" + scopeName + "' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton", ex);
                }
            }
        } catch (BeansException ex) {
            cleanupAfterBeanCreationFailure(beanName);
            throw ex;
        }
    }
    return (T) bean;
}
</code>

If the bean's scope is neither singleton nor prototype , Spring looks up the corresponding Scope implementation from the internal scopes map. If the custom scope is not registered, an exception is thrown.

2. Defining a Custom Scope

To create a custom scope, implement the org.springframework.beans.factory.config.Scope interface.

<code>public class CustomScope implements Scope {
    private Object target;

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        return target != null ? target : objectFactory.getObject();
    }

    @Override
    public Object remove(String name) {
        target = null;
        return "success";
    }

    @Override
    public void registerDestructionCallback(String name, Runnable callback) {
        // no-op
    }

    @Override
    public Object resolveContextualObject(String key) {
        return null;
    }

    @Override
    public String getConversationId() {
        return null;
    }
}
</code>

Register the custom scope with the bean factory, typically via a BeanDefinitionRegistryPostProcessor :

<code>@Component
public class CustomScopeRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        beanFactory.registerScope("custom", new CustomScope());
    }

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        // no additional processing needed
    }
}
</code>

Use the custom scope on a bean:

<code>@Component
@Scope("custom")
public class ApplyScopeBean {
    // bean logic
}
</code>

3. Prototype Injection into a Singleton

When a singleton bean depends on a prototype bean, you must enable a scoped proxy so that a new instance is obtained for each injection point.

<code>@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ApplyScopeBean {
    // prototype bean logic
}
</code>

Alternatively, you can request a new instance manually via BeanFactory#getBean() each time you need it.

4. Example Controller Using Custom and Prototype Scopes

<code>@RestController
@RequestMapping("/refresh")
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class RefreshController implements ApplicationContextAware {
    @Resource
    private ApplyScopeBean scopeBean;

    @Resource
    private CustomScope customScope;

    @GetMapping("/custom")
    public String custom() {
        return scopeBean.getCustom();
    }

    @GetMapping("/remove")
    public Object remove() {
        return customScope.remove("applyScopeBean");
    }
}
</code>

In this controller, the bean ApplyScopeBean is defined with a custom scope, and the controller itself is prototype‑scoped to demonstrate the lifecycle behavior. Deleting the bean via the remove endpoint forces Spring to create a new instance on the next request.

JavaSpringPrototypeBean ScopeCustom Scope
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.