Summary
A developer asked how to achieve dynamic loading and unloading of modules in a Spring Boot application to save memory, effectively asking for a “plug-in” architecture where beans are created and destroyed on demand.
The core issue is that the Spring IoC container is designed to be static after startup. While you can technically add beans, removing them cleanly is extremely difficult and generally unsupported.
The requested architecture contradicts the design philosophy of a standard Spring Boot monolith. The solution is not to fight the framework by forcing runtime unloading, but to embrace a Microservices or Serverless architecture where modules are physically separate processes that can be scaled to zero.
Root Cause
The root cause is a misunderstanding of the Spring ApplicationContext lifecycle.
- Singleton Scope: By default, Spring beans are singletons scoped to the
ApplicationContext. Once created and wired, they remain in memory until the application shuts down. - BeanFactory Limitations: The
BeanFactoryallows registration of new beans (viaBeanDefinition), but deregistering existing beans is not atomic or safe. You cannot guarantee that other beans aren’t holding references to the bean you want to remove. - Classloader Immutability: In a standard JVM environment, classes once loaded are rarely unloaded. Even if you “remove” the beans from Spring, the classes remain in memory (PermGen/Metaspace).
Why This Happens in Real Systems
This requirement usually stems from a desire to optimize resources within a single deployment unit. However, dynamic unloading in Java is fraught with peril:
- Memory Leaks: If a bean registers a listener or thread, or if another bean holds a reference to it, removing the bean definition does not remove the instance. This leads to “zombie” objects consuming memory.
- AOP Proxies: Spring creates proxies around beans (for transactions, security, caching). Deregistering a target bean requires complex manipulation of these proxies.
- Dependency Injection: If Service A depends on Module B, and Module B is unloaded, Service A becomes broken. You would need to rewrite Service A dynamically to handle the absence, which is a nightmare to maintain.
Real-World Impact
Attempting to force dynamic unloading in a monolith has severe consequences:
- Application Instability: It introduces race conditions where a request might try to access a bean that is mid-unload, causing
NoSuchBeanDefinitionExceptionorNullPointerException. - Classloader Hell: Trying to isolate modules to allow for unloading (using custom ClassLoaders) often leads to
ClassCastException(where the same class loaded by different loaders are seen as different types). - Wasted Engineering Time: It violates the “Share Nothing” architecture principle. Developers spend weeks debugging Spring internals instead of delivering business value.
Example or Code
While we strongly advise against this approach for production, here is an example of how Spring can technically add beans at runtime using the ConfigurableApplicationContext. Note the lack of a clean remove method.
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
public class DynamicBeanLoader {
public void loadNewModule(ApplicationContext context, String beanName, Class beanClass) {
if (context instanceof ConfigurableApplicationContext) {
ConfigurableApplicationContext configurableContext = (ConfigurableApplicationContext) context;
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) configurableContext.getBeanFactory();
// Create a new bean definition
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(beanName, beanClass);
BeanDefinition beanDefinition = builder.getBeanDefinition();
// Register it
beanFactory.registerBeanDefinition(beanName, beanDefinition);
// Now it exists and is autowired (if not eager)
Object instance = beanFactory.getBean(beanName);
}
}
}
How Senior Engineers Fix It
Seniors solve the “memory usage for unused modules” problem by changing the deployment topology, not by hacking the internal container. The recommended approaches are:
- Microservices (Physical Separation):
- Break the modules into separate Spring Boot applications (JARs).
- Deploy them as independent services (Docker containers).
- Benefit: If a module is rarely used, you can scale it to 0 replicas (Kubernetes HPA), achieving 100% resource savings. Communication happens via REST or Messaging.
- Spring Cloud Function (Serverless):
- Wrap the module logic in a functional interface.
- Deploy to AWS Lambda or similar.
- Benefit: The cloud provider handles the lifecycle. The code literally does not run (and costs $0) until a request triggers it.
- Modulith with Conditional Loading (Static):
- If the requirement is just “don’t start the beans,” use Spring Boot’s
ConditionalOnProperty. - This keeps the code in one project but prevents unused beans from instantiating at startup. Note: This does not support unloading later.
- If the requirement is just “don’t start the beans,” use Spring Boot’s
Why Juniors Miss It
Junior engineers often view the application as a bag of code rather than a managed lifecycle.
- Focus on Code, not Lifecycle: They look for a
removeBean()method, assuming Spring is as dynamic as a scripting language like Python or JS. - Underestimating Complexity: They don’t realize that in Java, “removing” a class implies complex Garbage Collection interactions, reference clearing, and potential deadlocks in concurrent environments.
- Over-optimizing Prematurely: They try to save memory within a single process before establishing that the application actually needs that optimization. Usually, a standard monolith fits in memory just fine.