coderain blog

Annotation-Based Factory Methods in Spring: How to Autowire a Singleton Bean Created by a Factory Method (No XML Required)

In Spring, beans are the backbone of any application, and managing their creation is a core responsibility of the Spring IoC (Inversion of Control) container. While Spring simplifies bean instantiation through auto-detection (e.g., @Component, @Service) for straightforward cases, complex scenarios—such as conditional initialization, dynamic configuration, or integration with legacy code—often require more control over bean creation. This is where factory methods shine.

Traditionally, factory methods in Spring were configured via XML, but modern Spring practices prioritize annotation-based configuration for its type safety, readability, and maintainability. In this blog, we’ll explore how to use annotation-based factory methods to create singleton beans and autowire them into other components—all without a single line of XML. By the end, you’ll master the @Bean annotation, understand how Spring manages factory methods, and confidently implement singleton beans with custom instantiation logic.

2025-12

Table of Contents#

  1. Understanding Factory Methods in Spring
  2. Why Use Annotation-Based Factory Methods?
  3. Prerequisites
  4. Setting Up the Project
  5. Step-by-Step Implementation
  6. Key Annotations Explained
  7. Verifying Singleton Scope
  8. Common Pitfalls and Best Practices
  9. Conclusion
  10. References

Understanding Factory Methods in Spring#

A factory method is a design pattern where a method (rather than a constructor) is responsible for creating and returning an instance of a class. In Spring, factory methods are used when:

  • Bean instantiation requires complex logic (e.g., conditional setup, reading configuration, or legacy class integration).
  • The bean type is determined dynamically at runtime.
  • The target class lacks a no-arg constructor or has non-trivial initialization steps.

Historically, Spring supported factory methods via XML configuration (e.g., using <bean factory-method="..."/>), but modern Spring applications leverage annotation-based configuration with @Configuration and @Bean annotations. These annotations turn Java methods into Spring-managed factory methods, eliminating the need for XML.

Why Use Annotation-Based Factory Methods?#

  • No XML Required: All configuration is done in Java, making it type-safe and easier to refactor.
  • Simplified Dependency Management: Spring automatically injects dependencies into factory methods (e.g., other beans or property values).
  • Singleton by Default: Beans created via @Bean methods are singletons by default, ensuring a single instance is reused throughout the application.
  • Integration with Spring Ecosystem: Seamlessly works with other annotations like @Autowired, @Value, and @Profile.

Prerequisites#

To follow along, ensure you have:

  • JDK 8 or higher
  • Maven or Gradle (we’ll use Maven)
  • A basic understanding of Spring IoC and dependency injection

Setting Up the Project#

Step 1: Create a Maven Project#

Add the following dependency to your pom.xml for Spring Core (which includes IoC container support):

<dependencies>
    <!-- Spring Core -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.20</version> <!-- Use the latest stable version -->
    </dependency>
</dependencies>

Step-by-Step Implementation#

Let’s build a sample application where a UserRepository bean is created via a factory method (due to complex initialization logic), and then autowired into a UserService.

Step 1: Define the Target Bean Class#

First, create the UserRepository class. It has a non-trivial constructor requiring a database URL and a read-only flag, simulating complex initialization.

// UserRepository.java
public class UserRepository {
    private final String dbUrl;
    private final boolean isReadOnly;
 
    // Complex constructor (not a no-arg constructor)
    public UserRepository(String dbUrl, boolean isReadOnly) {
        this.dbUrl = dbUrl;
        this.isReadOnly = isReadOnly;
        System.out.println("Initializing UserRepository with DB URL: " + dbUrl + ", Read-Only: " + isReadOnly);
    }
 
    // Getters for verification
    public String getDbUrl() { return dbUrl; }
    public boolean isReadOnly() { return isReadOnly; }
}

Step 2: Create a Configuration Class with Factory Methods#

Next, create a @Configuration class to host the factory method for UserRepository. The @Bean annotation marks a method as a factory method, and Spring will manage the returned object as a bean.

// AppConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.beans.factory.annotation.Value;
 
@Configuration // Marks this class as a source of bean definitions
public class AppConfig {
 
    // Factory method to create UserRepository (annotated with @Bean)
    @Bean // Default bean name is the method name ("userRepository")
    public UserRepository userRepository(
            @Value("${db.url:jdbc:mysql://localhost:3306/mydb}") String dbUrl, // Inject property or use default
            @Value("${db.readOnly:false}") boolean isReadOnly) { // Inject property or use default
        
        // Complex initialization logic (e.g., validate URL, setup connections)
        validateDbUrl(dbUrl);
        
        // Return the UserRepository instance
        return new UserRepository(dbUrl, isReadOnly);
    }
 
    // Helper method for validation (simulating complex logic)
    private void validateDbUrl(String dbUrl) {
        if (!dbUrl.startsWith("jdbc:")) {
            throw new IllegalArgumentException("Invalid DB URL: Must start with 'jdbc:'");
        }
    }
}

Key Details:#

  • @Configuration: Tells Spring this class contains bean definitions.
  • @Bean: Declares a factory method. The method name (userRepository) is the default bean ID (override with @Bean(name = "customName")).
  • @Value: Injects property values (from application.properties, environment variables, etc.). Default values (after :) are used if the property is undefined.
  • Dependency Injection: The userRepository method accepts parameters (dbUrl, isReadOnly), which Spring injects automatically (supports other beans too—e.g., a DataSource bean).

Step 3: Autowire the Factory-Created Bean#

Now, create a UserService that depends on UserRepository. Use @Autowired to inject the UserRepository bean created by our factory method.

// UserService.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
 
@Service // Marks this as a Spring-managed service bean
public class UserService {
    private final UserRepository userRepository;
 
    // Constructor injection (preferred for dependencies)
    @Autowired // Optional in Spring 4.3+ for single-arg constructors
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
        System.out.println("UserService initialized with UserRepository");
    }
 
    // Example method using the repository
    public void fetchUser() {
        System.out.println("Fetching user from: " + userRepository.getDbUrl());
    }
}

Step 4: Bootstrap the Spring Context#

Finally, create a main class to start the Spring IoC container and verify the setup.

// Main.java
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
public class Main {
    public static void main(String[] args) {
        // Initialize Spring context with annotation-based configuration
        try (ConfigurableApplicationContext context = 
                new AnnotationConfigApplicationContext(AppConfig.class)) {
 
            // Retrieve UserService (which depends on UserRepository)
            UserService userService = context.getBean(UserService.class);
            userService.fetchUser();
 
            // Verify singleton scope (see next section)
            verifySingletonScope(context);
        }
    }
 
    private static void verifySingletonScope(ConfigurableApplicationContext context) {
        UserRepository repo1 = context.getBean(UserRepository.class);
        UserRepository repo2 = context.getBean(UserRepository.class);
        System.out.println("Are UserRepository instances the same? " + (repo1 == repo2)); // true
    }
}

Add Configuration Properties (Optional)#

Create src/main/resources/application.properties to customize db.url and db.readOnly:

db.url=jdbc:postgresql://prod-db:5432/users
db.readOnly=true

If this file is missing, the @Value defaults (jdbc:mysql://localhost:3306/mydb and false) will be used.

Key Annotations Explained#

AnnotationPurpose
@ConfigurationMarks a class as a source of bean definitions. Spring processes @Bean methods here.
@BeanDeclares a factory method. Spring calls this method to create a bean, managing its lifecycle.
@ValueInjects property values (from files, environment variables, etc.). Supports default values.
@AutowiredInjects dependencies into constructors, fields, or methods. For UserService, it injects the UserRepository bean.
@ServiceA stereotype annotation marking UserService as a service-layer bean (auto-detected by Spring).

Verifying Singleton Scope#

Run the Main class. The output will show:

Initializing UserRepository with DB URL: jdbc:postgresql://prod-db:5432/users, Read-Only: true
UserService initialized with UserRepository
Fetching user from: jdbc:postgresql://prod-db:5432/users
Are UserRepository instances the same? true
  • UserRepository is initialized once (due to singleton scope).
  • repo1 == repo2 confirms the same instance is reused.

Common Pitfalls and Best Practices#

Pitfalls:#

  1. Missing @Configuration: If AppConfig lacks @Configuration, @Bean methods behave as "lite" factories—calling them directly creates new instances (bypassing singleton checks). Always use @Configuration for singleton guarantees.

  2. Overcomplicating Factory Methods: Avoid heavy logic in @Bean methods. Delegate complex setup to helper classes or use constructor injection for dependencies.

  3. Ignoring Default Scopes: @Bean methods default to singleton scope. Use @Scope("prototype") only if multiple instances are needed (rare for most applications).

Best Practices:#

  • Use Constructor Injection: Prefer constructor injection (as in UserService) over field injection for immutability and testability.
  • Name Beans Explicitly: Use @Bean(name = "customRepo") to avoid naming conflicts if multiple beans of the same type exist.
  • Document Factory Logic: Add comments to @Bean methods explaining why a factory is needed (e.g., "Complex DB setup for legacy system").

Conclusion#

Annotation-based factory methods in Spring (via @Configuration and @Bean) provide a powerful, XML-free way to create beans with custom initialization logic. By following this approach, you can:

  • Leverage type-safe, maintainable configuration.
  • Ensure singleton behavior for beans by default.
  • Seamlessly integrate with Spring’s dependency injection ecosystem.

Whether you’re handling complex initialization, dynamic bean creation, or legacy code, annotation-based factory methods simplify bean management while keeping your codebase clean and modern.

References#