Creating Spring Beans with Static Factory Methods

Spring beans are most commonly created through constructors or @Bean factory methods, but there is a third, lesser-known option: static factory methods. When a class uses the factory pattern to control its own instantiation — for example, a connection pool, a registry, or a singleton that validates its configuration at construction time — a static factory method lets Spring call that factory instead of a constructor. This keeps your code idiomatic and avoids adding unnecessary public constructors just to satisfy the IoC container.

Why Use a Static Factory Method?

  • Named construction — DatabaseConnection.of(config) is more readable than new DatabaseConnection(config).
  • Return type flexibility — a factory can return a subtype or interface implementation the caller doesn’t need to know about.
  • Construction-time validation — throw a descriptive exception before the object ever reaches the container.
  • Third-party classes — you cannot add @Component to a library class, but you can have Spring call its static factory method.

Approach 1 — XML Configuration (Classic)

In legacy Spring XML, use the factory-method attribute on a <bean> element:

<!-- Spring calls ConnectionPool.getInstance() instead of new ConnectionPool() -->
<bean id="connectionPool"
      class="com.ankurm.db.ConnectionPool"
      factory-method="getInstance">
    <constructor-arg value="jdbc:postgresql://localhost/mydb"/>
    <constructor-arg value="10"/>
</bean>

Approach 2 — @Bean Method (Recommended)

The modern, annotation-driven way is a @Bean method that delegates to the static factory:

package com.ankurm.db;

/**
 * A class that controls its own instantiation via a static factory.
 * Validates configuration before the object is created.
 */
public class ConnectionPool {

    private final String jdbcUrl;
    private final int    maxSize;

    // Private constructor — callers must use the factory
    private ConnectionPool(String jdbcUrl, int maxSize) {
        this.jdbcUrl = jdbcUrl;
        this.maxSize = maxSize;
    }

    /** Static factory with validation */
    public static ConnectionPool of(String jdbcUrl, int maxSize) {
        if (jdbcUrl == null || jdbcUrl.isBlank())
            throw new IllegalArgumentException("JDBC URL must not be blank");
        if (maxSize  200)
            throw new IllegalArgumentException("maxSize must be between 1 and 200");
        return new ConnectionPool(jdbcUrl, maxSize);
    }

    public String getJdbcUrl() { return jdbcUrl; }
    public int    getMaxSize() { return maxSize; }

    @Override
    public String toString() {
        return "ConnectionPool{url=" + jdbcUrl + ", maxSize=" + maxSize + "}";
    }
}
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DataSourceConfig {

    @Value("${db.url}")
    private String dbUrl;

    @Value("${db.pool.size:10}")
    private int poolSize;

    @Bean
    public ConnectionPool connectionPool() {
        // Spring calls the static factory method
        return ConnectionPool.of(dbUrl, poolSize);
    }
}

Approach 3 — Registering a Third-Party Class

A common real-world scenario: registering java.time.Clock as a bean for testability, using its static factory:

import java.time.Clock;
import java.time.ZoneId;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
public class ClockConfig {

    @Bean
    @Profile("!test")                       // production: real clock in UTC
    public Clock utcClock() {
        return Clock.system(ZoneId.of("UTC"));  // static factory
    }

    @Bean
    @Profile("test")                        // tests: fixed clock for determinism
    public Clock fixedClock() {
        return Clock.fixed(
            java.time.Instant.parse("2025-01-01T00:00:00Z"),
            ZoneId.of("UTC")
        );
    }
}
// Inject it anywhere
@Service
public class AuditService {

    private final Clock clock;

    public AuditService(Clock clock) {
        this.clock = clock;         // production = real, test = fixed
    }

    public java.time.Instant now() {
        return java.time.Instant.now(clock);
    }
}

Approach 4 — Static Factory with Multiple Beans

Static factories can also produce different implementations based on a configuration property:

public interface CacheProvider {
    void put(String key, Object value);
    Object get(String key);

    static CacheProvider of(String type) {
        return switch (type.toLowerCase()) {
            case "redis"   -> new RedisCacheProvider();
            case "hazelcast" -> new HazelcastCacheProvider();
            default        -> new InMemoryCacheProvider();
        };
    }
}

@Configuration
public class CacheConfig {

    @Value("${cache.provider:inmemory}")
    private String cacheType;

    @Bean
    public CacheProvider cacheProvider() {
        return CacheProvider.of(cacheType);   // returns the right impl at runtime
    }
}

Unit Testing with a Static Factory Bean

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@TestPropertySource(properties = {
    "db.url=jdbc:h2:mem:testdb",
    "db.pool.size=5"
})
class ConnectionPoolTest {

    @Autowired
    ConnectionPool pool;

    @Test
    void poolShouldBeCreated() {
        assertNotNull(pool);
        assertEquals("jdbc:h2:mem:testdb", pool.getJdbcUrl());
        assertEquals(5, pool.getMaxSize());
    }

    @Test
    void factoryShouldRejectBlankUrl() {
        assertThrows(IllegalArgumentException.class,
            () -> ConnectionPool.of("", 10));
    }

    @Test
    void factoryShouldRejectOversizedPool() {
        assertThrows(IllegalArgumentException.class,
            () -> ConnectionPool.of("jdbc:h2:mem:test", 999));
    }
}

See Also

Conclusion

Static factory methods give you named, validated, and readable object construction. Spring integrates with them naturally via @Bean methods that simply call the factory — no special configuration required. Use this pattern when a class controls its own instantiation, when you need to register a third-party class you cannot annotate, or when you want to return different implementations based on runtime configuration. Pair the factory bean with profile-specific overrides for a clean, testable architecture.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.