coderain blog

How to Implement Annotation-Based ServiceLocatorFactoryBean in Spring: A Step-by-Step Guide to Factory Pattern with Annotations (Instead of XML)

In Spring, managing dependencies efficiently is crucial for building scalable and maintainable applications. While @Autowired and @Qualifier are go-to tools for dependency injection, they fall short when you need dynamic service resolution—i.e., selecting a service implementation at runtime based on a condition (e.g., a user input or configuration parameter). This is where the Factory Pattern shines, and Spring’s ServiceLocatorFactoryBean is the perfect tool to implement it without XML configuration.

Traditionally, ServiceLocatorFactoryBean was configured via XML, but modern Spring applications favor annotation-based (JavaConfig) setups for better readability and maintainability. In this guide, we’ll walk through implementing ServiceLocatorFactoryBean using annotations, leveraging Spring’s power to create a flexible, dynamic service factory.

2025-12

Table of Contents#

  1. Prerequisites
  2. Understanding ServiceLocatorFactoryBean and the Factory Pattern
  3. Step-by-Step Implementation
  4. Common Pitfalls and Best Practices
  5. Conclusion
  6. References

Prerequisites#

Before diving in, ensure you have:

  • Basic knowledge of Java and Spring Core (dependency injection, @Service, @Configuration).
  • Familiarity with the Factory Design Pattern.
  • A Java IDE (IntelliJ IDEA, Eclipse, etc.).
  • Maven or Gradle (we’ll use Maven here).
  • JDK 11 or higher.

Understanding ServiceLocatorFactoryBean and the Factory Pattern#

The Factory Pattern Recap#

The Factory Pattern is a creational design pattern that provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created. In Spring, this translates to a "service factory" that dynamically returns the right service implementation based on input (e.g., a string, enum, or condition).

What is ServiceLocatorFactoryBean?#

ServiceLocatorFactoryBean is a Spring-specific factory bean that acts as a dynamic service locator. It generates a proxy for a user-defined "locator interface" and uses Spring’s BeanFactory to resolve and return the appropriate service bean by name at runtime.

Key Benefits:#

  • Dynamic Resolution: Unlike @Autowired (which injects dependencies at startup), ServiceLocatorFactoryBean resolves services on-demand, making it ideal for scenarios where the required service isn’t known until runtime.
  • Loose Coupling: Decouples the client from concrete service implementations; clients depend only on the locator interface.
  • Annotation Support: Modern Spring applications can configure it via JavaConfig (no XML required!).

When to Use It?#

Use ServiceLocatorFactoryBean when:

  • You need to select a service implementation dynamically (e.g., payment processors, notification services).
  • You want to avoid hardcoding dependencies with @Qualifier or @Resource.
  • You need to manage a family of related services (e.g., different report generators).

Step-by-Step Implementation#

Let’s build a practical example: a payment processing system that supports multiple payment methods (e.g., Credit Card, PayPal, Bitcoin). The factory will return the appropriate PaymentProcessor based on a user’s input (e.g., "creditCard", "paypal").

Step 1: Set Up the Project#

We’ll use Spring Boot for simplicity (it auto-configures the BeanFactory and reduces boilerplate).

Maven pom.xml#

Add these dependencies to your pom.xml:

<?xml version="1.0" encoding="UTF-8"?>  
<project xmlns="http://maven.apache.org/POM/4.0.0"  
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">  
    <modelVersion>4.0.0</modelVersion>  
    <parent>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-parent</artifactId>  
        <version>3.2.0</version>  
        <relativePath/> <!-- lookup parent from repository -->  
    </parent>  
    <groupId>com.example</groupId>  
    <artifactId>service-locator-demo</artifactId>  
    <version>0.0.1-SNAPSHOT</version>  
    <name>service-locator-demo</name>  
    <description>Demo project for ServiceLocatorFactoryBean with Annotations</description>  
 
    <properties>  
        <java.version>17</java.version>  
    </properties>  
 
    <dependencies>  
        <!-- Spring Boot Core -->  
        <dependency>  
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-starter</artifactId>  
        </dependency>  
 
        <!-- Testing -->  
        <dependency>  
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-starter-test</artifactId>  
            <scope>test</scope>  
        </dependency>  
    </dependencies>  
 
    <build>  
        <plugins>  
            <plugin>  
                <groupId>org.springframework.boot</groupId>  
                <artifactId>spring-boot-maven-plugin</artifactId>  
            </plugin>  
        </plugins>  
    </build>  
</project>  

Step 2: Define Service Interface and Implementations#

First, create a PaymentProcessor interface for all payment methods:

// com/example/servicelocator/service/PaymentProcessor.java  
package com.example.servicelocator.service;  
 
public interface PaymentProcessor {  
    String processPayment(double amount);  
}  

Next, implement concrete payment processors. Annotate each with @Service and assign a unique bean name (this name will be used by the locator to resolve the service):

Credit Card Processor#

// com/example/servicelocator/service/CreditCardPaymentProcessor.java  
package com.example.servicelocator.service;  
 
import org.springframework.stereotype.Service;  
 
@Service("creditCard") // Bean name: "creditCard"  
public class CreditCardPaymentProcessor implements PaymentProcessor {  
 
    @Override  
    public String processPayment(double amount) {  
        return String.format("Processing $%.2f via Credit Card. Transaction ID: %s",  
                amount, generateTransactionId());  
    }  
 
    private String generateTransactionId() {  
        return "CC-" + System.currentTimeMillis();  
    }  
}  

PayPal Processor#

// com/example/servicelocator/service/PayPalPaymentProcessor.java  
package com.example.servicelocator.service;  
 
import org.springframework.stereotype.Service;  
 
@Service("paypal") // Bean name: "paypal"  
public class PayPalPaymentProcessor implements PaymentProcessor {  
 
    @Override  
    public String processPayment(double amount) {  
        return String.format("Processing $%.2f via PayPal. Transaction ID: %s",  
                amount, generateTransactionId());  
    }  
 
    private String generateTransactionId() {  
        return "PP-" + System.currentTimeMillis();  
    }  
}  

Bitcoin Processor (Optional)#

Add another processor to extend the example:

// com/example/servicelocator/service/BitcoinPaymentProcessor.java  
package com.example.servicelocator.service;  
 
import org.springframework.stereotype.Service;  
 
@Service("bitcoin") // Bean name: "bitcoin"  
public class BitcoinPaymentProcessor implements PaymentProcessor {  
 
    @Override  
    public String processPayment(double amount) {  
        return String.format("Processing $%.2f via Bitcoin. Transaction ID: %s",  
                amount, generateTransactionId());  
    }  
 
    private String generateTransactionId() {  
        return "BTC-" + System.currentTimeMillis();  
    }  
}  

Step 3: Create the Service Locator Interface#

Define a locator interface with a method to fetch the PaymentProcessor by name. Spring will generate a proxy for this interface using ServiceLocatorFactoryBean.

// com/example/servicelocator/locator/PaymentProcessorLocator.java  
package com.example.servicelocator.locator;  
 
import com.example.servicelocator.service.PaymentProcessor;  
 
public interface PaymentProcessorLocator {  
    // Method to get PaymentProcessor by bean name (e.g., "creditCard", "paypal")  
    PaymentProcessor getPaymentProcessor(String processorName);  
}  

Note: The method name (getPaymentProcessor) and parameter (String processorName) are critical. Spring uses this method to resolve the bean by name.

Step 4: Configure ServiceLocatorFactoryBean with Annotations#

Now, configure ServiceLocatorFactoryBean via JavaConfig. Create a @Configuration class to define the factory bean:

// com/example/servicelocator/config/ServiceLocatorConfig.java  
package com.example.servicelocator.config;  
 
import com.example.servicelocator.locator.PaymentProcessorLocator;  
import org.springframework.beans.factory.config.ServiceLocatorFactoryBean;  
import org.springframework.context.annotation.Bean;  
import org.springframework.context.annotation.Configuration;  
 
@Configuration  
public class ServiceLocatorConfig {  
 
    @Bean  
    public ServiceLocatorFactoryBean paymentProcessorLocator() {  
        ServiceLocatorFactoryBean factoryBean = new ServiceLocatorFactoryBean();  
        // Set the locator interface (Spring will generate a proxy for this)  
        factoryBean.setServiceLocatorInterface(PaymentProcessorLocator.class);  
        return factoryBean;  
    }  
}  

What’s Happening?

  • ServiceLocatorFactoryBean is registered as a Spring bean.
  • setServiceLocatorInterface tells Spring to generate a proxy for PaymentProcessorLocator, which will use Spring’s BeanFactory to resolve PaymentProcessor beans by name.

Step 5: Inject and Use the Service Locator#

Create a PaymentService to use the locator and process payments dynamically:

// com/example/servicelocator/service/PaymentService.java  
package com.example.servicelocator.service;  
 
import com.example.servicelocator.locator.PaymentProcessorLocator;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.stereotype.Service;  
 
@Service  
public class PaymentService {  
 
    private final PaymentProcessorLocator paymentProcessorLocator;  
 
    // Inject the locator (Spring injects the proxy generated by ServiceLocatorFactoryBean)  
    @Autowired  
    public PaymentService(PaymentProcessorLocator paymentProcessorLocator) {  
        this.paymentProcessorLocator = paymentProcessorLocator;  
    }  
 
    public String processPayment(String processorType, double amount) {  
        // Use the locator to fetch the processor by name (e.g., "creditCard")  
        PaymentProcessor processor = paymentProcessorLocator.getPaymentProcessor(processorType);  
        return processor.processPayment(amount);  
    }  
}  

Key Point: The PaymentProcessorLocator is injected like any other Spring bean. Spring provides the proxy implementation at runtime.

Step 6: Test the Implementation#

Let’s verify the setup with a test. Use JUnit 5 and Spring Boot Test to inject PaymentService and test dynamic resolution.

// com/example/servicelocator/ServiceLocatorDemoApplicationTests.java  
package com.example.servicelocator;  
 
import com.example.servicelocator.service.PaymentService;  
import org.junit.jupiter.api.Test;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.boot.test.context.SpringBootTest;  
import static org.junit.jupiter.api.Assertions.*;  
 
@SpringBootTest  
class ServiceLocatorDemoApplicationTests {  
 
    @Autowired  
    private PaymentService paymentService;  
 
    @Test  
    void testCreditCardPayment() {  
        String result = paymentService.processPayment("creditCard", 99.99);  
        assertTrue(result.contains("Credit Card"));  
        assertTrue(result.contains("CC-")); // Transaction ID starts with CC-  
    }  
 
    @Test  
    void testPayPalPayment() {  
        String result = paymentService.processPayment("paypal", 149.99);  
        assertTrue(result.contains("PayPal"));  
        assertTrue(result.contains("PP-"));  
    }  
 
    @Test  
    void testBitcoinPayment() {  
        String result = paymentService.processPayment("bitcoin", 299.99);  
        assertTrue(result.contains("Bitcoin"));  
        assertTrue(result.contains("BTC-"));  
    }  
 
    @Test  
    void testUnknownProcessor() {  
        assertThrows(IllegalStateException.class, () ->  
            paymentService.processPayment("applePay", 49.99)  
        );  
    }  
}  

Explanation:

  • Tests verify that the correct PaymentProcessor is returned for "creditCard", "paypal", and "bitcoin".
  • The testUnknownProcessor checks that an exception is thrown for an invalid processor name (since no bean named "applePay" exists).

Common Pitfalls and Best Practices#

Pitfalls to Avoid#

  1. Incorrect Bean Names: The @Service bean name must match the string passed to the locator (e.g., @Service("creditCard") and locator.getPaymentProcessor("creditCard")). Mismatched names cause NoSuchBeanDefinitionException.

  2. Locator Interface Method Signature: The locator method must return the service interface (e.g., PaymentProcessor) and take a single String parameter (the bean name). Invalid signatures break proxy generation.

  3. Missing Service Implementations: If a processor type is requested but no @Service with that name exists, Spring throws NoSuchBeanDefinitionException. Always validate input!

  4. Overusing ServiceLocator: Avoid using it for static dependencies. Stick to @Autowired when the service is known at startup.

Best Practices#

  • Use Enums for Safety: Replace raw strings with enums (e.g., PaymentMethod.CREDIT_CARD) to avoid typos. Convert the enum to a string (e.g., paymentMethod.name().toLowerCase()) when calling the locator.

    public enum PaymentMethod { CREDIT_CARD, PAYPAL, BITCOIN }  
    // In PaymentService:  
    public String processPayment(PaymentMethod method, double amount) {  
        return paymentProcessorLocator.getPaymentProcessor(method.name().toLowerCase())  
                                      .processPayment(amount);  
    }  
  • Add Fallback Mechanism: Handle unknown processor types gracefully (e.g., return a default processor or throw a custom exception).

  • Document the Locator Interface: Clearly document the expected bean names (e.g., "Valid processor names: 'creditCard', 'paypal', 'bitcoin'").

Conclusion#

ServiceLocatorFactoryBean is a powerful tool for implementing the Factory Pattern in Spring with annotations. It enables dynamic service resolution, reduces coupling, and aligns with modern JavaConfig practices. By following this guide, you’ve learned how to:

  • Define service interfaces and implementations.
  • Create a locator interface for dynamic resolution.
  • Configure ServiceLocatorFactoryBean without XML.
  • Inject and use the locator to fetch services at runtime.

Use this pattern to build flexible, maintainable applications where service implementations need to be swapped or selected dynamically!

References#