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.

Syntax@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

  1. A property, such as spring.datasource.hikari.maximum-pool-size. Smallest change, visible in configuration.
  2. A customiser bean, such as JsonMapperBuilderCustomizer or RestClientCustomizer. Adjusts Boot's object and keeps the rest of its setup.
  3. 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.
  4. Exclusion, with @SpringBootApplication(exclude = ...) or spring.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

Example · java

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;

@ConfigurationProperties("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.
@AutoConfiguration
@EnableConfigurationProperties(ClockProperties.class)
@ConditionalOnProperty(name = "app.clock.enabled", havingValue = "true", matchIfMissing = true)
public class ClockAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    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.

Example · text
# src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.platform.clock.ClockAutoConfiguration

Testing 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.

Example · java
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.

Example · properties
# 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

  • Be the first to comment on this lesson.