On-Demand Beans in Spring Boot: Scalable patterns in Spring

If you need to dynamically select or create a Spring bean at runtime, avoid calling ApplicationContext#getBean() inside business code.
Instead, use one of these clean, scalable patterns:
✅ Map / EnumMap Injection (Strategy + Factory) – default and most idiomatic
✅ ServiceLocatorFactoryBean – proxy-based, zero-boilerplate lookup
✅ @Lookup Method Injection – fresh prototype per call
✅ FactoryBean<T> – encapsulated complex object creation
This article explains when to use each pattern, with full code examples, pros/cons, and real-world scenarios.
WHY YOU SHOULD AVOID ApplicationContext#getBean()
Yes, this works:
applicationContext.getBean(“emailService”);
But it comes with problems:
❌ Tight coupling to Spring container
❌ Hidden dependencies
❌ Harder unit testing
❌ String-based lookup errors
❌ Violates clean architecture boundaries
Your domain logic should not know about the container.
Spring provides better, cleaner patterns.
Let’s explore them.
PATTERN 1: Strategy + Factory Using Map Injection (Most Recommended)
✅ Use When
You have multiple implementations of an interface and need to select one based on a key (string or enum).
This is the simplest, most explicit, and most scalable solution.
Step 1: Define the Interface
public interface NotificationService {
void send(String to, String message);
String key(); // e.g., “EMAIL”, “SMS”
}
Step 2: Implementations
@Service
class EmailNotificationService implements NotificationService {
@Override
public void send(String to, String message) {
System.out.println(“Sending EMAIL to ” + to);
}
@Override
public String key() {
return “EMAIL”;
}
}
@Service
class SmsNotificationService implements NotificationService {
@Override
public void send(String to, String message) {
System.out.println(“Sending SMS to ” + to);
}
@Override
public String key() {
return “SMS”;
}
}
Step 3: Factory (Build Map at Startup)
@Component
public class NotificationServiceFactory {
private final Map<String, NotificationService> byKey;
public NotificationServiceFactory(List<NotificationService> services) {
this.byKey = new HashMap<>();
services.forEach(s -> byKey.put(s.key(), s));
}
public NotificationService get(String key) {
var svc = byKey.get(key);
if (svc == null) {
throw new IllegalArgumentException(“No NotificationService for key: ” + key);
}
return svc;
}
}
Step 4: Usage
@Service
public class NotificationFacade {
private final NotificationServiceFactory factory;
public NotificationFacade(NotificationServiceFactory factory) {
this.factory = factory;
}
public void notify(String channel, String to, String msg) {
factory.get(channel).send(to, msg);
}
}
Pros
✔ Clear and explicit
✔ Easy to unit test
✔ No runtime magic
✔ Open for extension
Cons
✖ Small amount of boilerplate
Enum-Safe Variant (Recommended in Production)
public enum Channel {
EMAIL,
SMS
}
public interface NotificationService {
Channel key();
void send(String to, String message);
}
@Component
public class NotificationServiceFactory {
private final Map<Channel, NotificationService> byKey;
public NotificationServiceFactory(List<NotificationService> services) {
this.byKey = new EnumMap<>(Channel.class);
services.forEach(s -> byKey.put(s.key(), s));
}
public NotificationService get(Channel key) {
var svc = byKey.get(key);
if (svc == null) {
throw new IllegalArgumentException(“No service for key: ” + key);
}
return svc;
}
}
This should be your default pattern.
PATTERN 2: ServiceLocatorFactoryBean (Interface-Driven Dynamic Lookup)
✅ Use When
* You want zero factory boilerplate
* You prefer an interface-based contract
* You’re building plugin-style systems
Step 1: Define Locator Interface
public interface NotificationServiceLocator {
NotificationService getService(String beanName);
}
Step 2: Configuration
@Configuration
public class LocatorConfig {
@Bean
public FactoryBean<?> notificationServiceLocator() {
ServiceLocatorFactoryBean fb = new ServiceLocatorFactoryBean();
fb.setServiceLocatorInterface(NotificationServiceLocator.class);
return fb;
}
}
Step 3: Usage
@Service
public class NotificationFacade {
private final NotificationServiceLocator locator;
public NotificationFacade(NotificationServiceLocator locator) {
this.locator = locator;
}
public void notify(String beanName, String to, String msg) {
locator.getService(beanName).send(to, msg);
}
}
Pros:
* Zero factory code
* Interface-driven
* Easy to mock
* Works with prototype beans
Cons:
* Bean-name strings can cause typos
* Slightly magical for beginners
PATTERN 3: @Lookup Method Injection (Prototype Per Call)
✅ Use When
You need a new prototype instance per method call, but the caller is a singleton.
Prototype Bean
@Component
@Scope(“prototype”)
public class AgentTaskHandler {
public void handle(String input) {
System.out.println(“Handling task: ” + input);
}
}
Singleton Caller
@Service
public class AgentService {
public void runTask(String input) {
AgentTaskHandler handler = createHandler();
handler.handle(input);
}
@Lookup(“agentTaskHandler”)
protected AgentTaskHandler createHandler() {
return null; // Spring overrides this method
}
}
Pros:
* Clean prototype retrieval
* Keeps singletons stateless
Cons:
* Runtime method override
* Can confuse new developers
PATTERN 4: FactoryBean (Encapsulated Complex Creation)
✅ Use When
You need to create:
* SDK clients
* External connectors
* Conditional constructors
* Non-Spring objects
@Component
public class ExternalClientFactory implements FactoryBean<ExternalClient> {
@Override
public ExternalClient getObject() {
return new ExternalClient(“endpoint”, “apiKey”);
}
@Override
public Class<?> getObjectType() {
return ExternalClient.class;
}
}
Now you can inject:
@Autowired
private ExternalClient client;
Spring injects the created object — not the factory.
Pros:
* Encapsulates complex construction
* Clean dependency injection
* Lifecycle managed by Spring
Cons:
* Slightly advanced concept
REAL-WORLD EXAMPLE: Vendor-Based Dispatch
Imagine your notification system supports:
* SMS
* EMAIL
* TWITTER
Based on vendor type, you must dispatch to the correct implementation.
Best Fit:
* EnumMap Injection (compile-time safety)
If adding modules dynamically:
* Consider ServiceLocatorFactoryBean
If handlers are stateful:
* Combine with @Lookup
TESTING TIPS
Map Injection:
Inject test map with mocks
Service Locator:
Mock locator interface
@Lookup:
Override method in test subclass
FactoryBean:
Unit test factory separately
COMMON PITFALLS
* Calling ApplicationContext#getBean() in business logic
* Using strings instead of enums
* Mixing business logic inside factories
* Holding mutable state in singletons
* Not documenting valid keys
WHICH PATTERN SHOULD YOU CHOOSE?
Select implementation by key:
Map / EnumMap Injection
Plugin-style dynamic lookup:
ServiceLocatorFactoryBean
New instance per call:
@Lookup
Complex object creation:
FactoryBean
FINAL THOUGHTS
You do not need to manually create Spring beans in your business logic.
Spring already provides elegant, scalable patterns:
* Map / EnumMap Injection → default and recommended
* ServiceLocatorFactoryBean → interface-driven lookup
* @Lookup → fresh prototype per call
* FactoryBean → encapsulated object creation
Start simple.
Keep dependencies explicit.
Avoid container leakage.
That’s how you build scalable Spring Boot systems.
BONUS: COPY-PASTE TEMPLATE
public enum Channel { EMAIL, SMS }
public interface NotificationService {
Channel key();
void send(String to, String message);
}
@Service
class EmailNotificationService implements NotificationService {
@Override
public Channel key() { return Channel.EMAIL; }
@Override
public void send(String to, String message) { }
}
@Service
class SmsNotificationService implements NotificationService {
@Override
public Channel key() { return Channel.SMS; }
@Override
public void send(String to, String message) { }
}
@Component
public class NotificationServiceFactory {
private final Map<Channel, NotificationService> byKey;
public NotificationServiceFactory(List<NotificationService> services) {
this.byKey = new EnumMap<>(Channel.class);
services.forEach(s -> byKey.put(s.key(), s));
}
public NotificationService get(Channel channel) {
var svc = byKey.get(channel);
if (svc == null) throw new IllegalArgumentException(“No service for ” + channel);
return svc;
}
}