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 thannew 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
@Componentto 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
- Building a REST API with Spring Boot
- Resilience4j Circuit Breaker in Spring Boot
- JMS Deep Dive: Asynchronous Messaging in Java
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.