Starters and Auto-configuration
How starters bring in libraries, how auto-configuration decides which beans to create, and how to override, exclude and debug it.
@AutoConfiguration
@ConditionalOnClass(SomeLibrary.class)
class SomeAutoConfiguration {
@Bean
@ConditionalOnMissingBean
SomeLibrary someLibrary() { ... }
}A starter is a dependency with almost no code of its own. It declares a set of libraries that belong together. spring-boot-starter-webmvc brings Spring MVC, Jackson, embedded Tomcat and Boot's web module; spring-boot-starter-data-jpa brings Hibernate, Spring Data JPA, the JDBC module and HikariCP. Boot's dependency management fixes every version, so the set you get has been tested together.
Where the auto-configuration lives
In Boot 4 each technology has its own module, and the module holds that technology's auto-configuration. The JDBC module contains org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration, the web module WebMvcAutoConfiguration, and so on. Each module lists its auto-configuration classes in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports, and @EnableAutoConfiguration reads every such file on the classpath.
Conditions decide
An auto-configuration class is an ordinary configuration class guarded by conditions:
@ConditionalOnClass: only if a library is on the classpath.@ConditionalOnProperty: only if a property has a certain value.@ConditionalOnMissingBean: only if you have not defined a bean of that type yourself.
The last one is why Boot steps aside for your beans. Auto-configurations run after your own configuration is registered, so they can see what you declared.
Changing what you get, in order of preference
- A property, such as
spring.datasource.hikari.maximum-pool-size. Smallest change, visible in configuration. - A customiser bean, such as
JsonMapperBuilderCustomizerorRestClientCustomizer. Adjusts Boot's object and keeps the rest of its setup. - Your own bean of the same type. Boot backs off completely, and you now own every setting it would have applied, which is easy to forget.
- Exclusion, with
@SpringBootApplication(exclude = ...)orspring.autoconfigure.exclude.
Debugging
Start the application with --debug or set debug=true to print the conditions evaluation report: every auto-configuration, whether it matched, and the condition that decided. With Actuator, the conditions endpoint returns the same report from a running instance. Read it whenever a bean is unexpectedly present or missing; guessing is slower.
A frequent production surprise: a transitive dependency puts a library on the classpath, its auto-configuration activates, and the application suddenly creates a Redis or broker client, reports a failing health check or tries to connect at start-up. Check ./mvnw dependency:tree after adding any dependency.
Example
import java.time.Clock;
import java.time.ZoneId;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
("app.clock")
record ClockProperties(String zone) {
ClockProperties {
if (zone == null) {
zone = "UTC";
}
}
}
// A shared library's auto-configuration: every service gets a Clock bean unless it opts out.
(ClockProperties.class)
(name = "app.clock.enabled", havingValue = "true", matchIfMissing = true)
public class ClockAutoConfiguration {
Clock clock(ClockProperties properties) {
// A service that declares its own Clock (a fixed one in tests, say) wins.
return Clock.system(ZoneId.of(properties.zone()));
}
}
When to use it
- Find out why the application tries to connect to Redis at start-up when no code uses it.
- Ship a shared company library that configures a bean in every service unless the service defines its own.
- Raise the connection pool size with one property instead of replacing the DataSource bean.
More examples
Registering the auto-configuration
One fully qualified class name per line. Boot reads this file from every jar on the classpath, which is exactly how its own modules register their auto-configuration. Do not also put the class under component scanning: it would then be processed as ordinary configuration, too early to see the application's own beans, and @ConditionalOnMissingBean would give the wrong answer.
# src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.platform.clock.ClockAutoConfigurationTesting the conditions with ApplicationContextRunner
ApplicationContextRunner builds a small context per test in milliseconds, with no server. It is the standard way to test that an auto-configuration matches when it should and backs off when it should. Testing the back-off case matters most: it is the promise your library makes to every service that uses it.
import static org.assertj.core.api.Assertions.assertThat;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
class ClockAutoConfiguration {
@Bean
@ConditionalOnMissingBean
Clock clock() {
return Clock.system(ZoneId.of("UTC"));
}
}
class ClockAutoConfigurationTest {
private final ApplicationContextRunner runner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(ClockAutoConfiguration.class));
@Test
void providesClockByDefault() {
runner.run(context -> assertThat(context).hasSingleBean(Clock.class));
}
@Test
void backsOffWhenServiceDefinesItsOwn() {
Clock fixed = Clock.fixed(Instant.parse("2026-01-01T00:00:00Z"), ZoneOffset.UTC);
runner.withBean(Clock.class, () -> fixed)
.run(context -> assertThat(context.getBean(Clock.class)).isSameAs(fixed));
}
}Printing the conditions report
With debug=true the start-up log ends with Positive matches, Negative matches and Exclusions, each with the condition that decided. Search it for the class you care about. The exclude property takes fully qualified names, and in Boot 4 those names use the new module packages, so an exclusion copied from a Boot 3 project silently matches nothing.
# Equivalent to starting with --debug. Remove before committing.
debug=true
# Switch off an auto-configuration without touching code (comma-separated list).
spring.autoconfigure.exclude=org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration
Discussion