How Spring Boot Auto-Configuration Actually Works

3 min read

  • Spring Boot
  • Java
  • Configuration

Add spring-boot-starter-web and you get an embedded Tomcat, a DispatcherServlet, Jackson and a dozen other beans you never declared. Add a DataSource property and a connection pool appears. This is auto-configuration, and it is not magic, it is a list of classes with conditions on them.

Understanding the mechanism is what lets you override it without fighting it.

Where the list comes from

Every auto-configuration jar contains a file at META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports, listing candidate configuration classes one per line. @SpringBootApplication includes @EnableAutoConfiguration, which reads every copy of that file on the classpath and processes what it finds.

(If you are looking at older code or older answers online, this used to live under a key in spring.factories. That form was removed in Spring Boot 3.)

Each class in that list is an ordinary @Configuration class carrying conditions:

@AutoConfiguration
@ConditionalOnClass(DataSource.class)
@ConditionalOnMissingBean(DataSource.class)
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {

    @Bean
    public DataSource dataSource(DataSourceProperties properties) { ... }
}

Read that as three statements. Only apply this if DataSource is on the classpath. Only apply it if nobody has already defined one. And bind configuration into a properties object while doing so.

The conditions worth knowing

@ConditionalOnClass and @ConditionalOnMissingClass test the classpath. This is how a starter can react to a library being present without depending on it, and it is why adding a dependency changes behaviour with no code change.

@ConditionalOnBean and @ConditionalOnMissingBean test the context. @ConditionalOnMissingBean is the important one: it is the mechanism by which your own bean wins over the framework's. Define your own ObjectMapper and Boot steps aside, because its definition was conditional on you not having one.

@ConditionalOnProperty tests configuration:

@Bean
@ConditionalOnProperty(name = "features.export.enabled", havingValue = "true")
public ExportScheduler exportScheduler() { ... }

Add matchIfMissing = true when the feature should default to on. This is a much better feature toggle than an if inside the bean, because a disabled feature costs nothing at runtime and its dependencies never get created.

@Profile is the older, coarser sibling. It matches on the active profile rather than a property, which is covered in more detail in Spring Boot profiles and configuration.

@ConditionalOnWebApplication distinguishes a servlet stack from a reactive one from neither.

Ordering is not incidental

Conditions on beans are evaluated in order, so "is there already a DataSource" depends on whether the thing that would define one has run yet. Auto-configuration classes always run after user configuration, which is what makes @ConditionalOnMissingBean reliable. Between themselves they use explicit ordering:

@AutoConfiguration(after = DataSourceAutoConfiguration.class)
public class JdbcTemplateAutoConfiguration { ... }

Two consequences worth internalising. @ConditionalOnBean inside your own @Configuration is fragile, because your classes have no guaranteed order relative to each other. And @ConditionalOnMissingBean in your own code is usually a sign you are writing a library rather than an application.

Seeing what happened

When a bean you expected is missing, stop guessing:

./gradlew bootRun --args='--debug'

The --debug flag prints the condition evaluation report: every auto-configuration class, split into positive matches, negative matches with the exact condition that failed, and exclusions. The negative section is the useful half, and it reads like Did not match: @ConditionalOnClass did not find required class 'javax.sql.DataSource'.

If Actuator is on the classpath, the same report is available at /actuator/conditions on a running application, which is easier to search.

Overriding, in order of preference

Set a property. Most auto-configuration is tuned by configuration, and the properties are documented. Check this before anything else.

Define the bean yourself. @ConditionalOnMissingBean means your definition simply wins. No exclusion, no flag, no ordering.

Exclude the auto-configuration. For when it should not run at all:

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

This is the blunt instrument. Excluding one class often removes others that were conditional on its beans, and the failure appears somewhere unrelated.

Writing your own

If you maintain shared libraries across services, this is worth doing rather than copying configuration classes between repositories. Create the configuration class, annotate it with @AutoConfiguration and the right conditions, list it in the .imports file, and make every bean @ConditionalOnMissingBean so consumers can override it.

The discipline that makes such a library pleasant to use is exactly the discipline Boot follows: never define a bean the application might want to define itself, and make everything switchable with a property.