Skip to content
JavaAgentic

Type at least two characters. Try “RAG”, “pgvector” or “tool calling”.

Spring Boot Auto-Configuration Deep Dive

How Spring Boot auto-configuration actually works: the import selector, the @Conditional family, ordering rules, the --debug report, and how to write your own starter.

Intermediate6 min readUpdated
On this page

Spring Boot's reputation for magic comes almost entirely from one mechanism. Understand it and the magic becomes a fairly ordinary piece of engineering: a list of candidate configuration classes, each wrapped in a set of conditions, evaluated in a defined order. This guide takes that mechanism apart and then puts it back together as a custom starter.

Key Takeaways

  • Auto-configuration is just @Configuration classes listed in a file, filtered by conditions.
  • @ConditionalOnMissingBean is what makes Boot back off when you define your own bean — it is the entire override mechanism.
  • Auto-configuration runs after your own configuration, which is why your beans win.
  • --debug prints a condition evaluation report that answers almost every "why is this bean here / not here" question in seconds.
  • A custom starter is two modules: an autoconfigure module with the conditions, and a thin starter module that only carries dependencies.

The mechanism, end to end

@SpringBootApplication is a composed annotation, and the interesting third of it is @EnableAutoConfiguration. That annotation imports AutoConfigurationImportSelector, which reads every META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports file on the classpath, deduplicates the class names, removes exclusions, sorts them, and hands the surviving list to the context as additional configuration classes.

From @SpringBootApplication to a registered bean: the auto-configuration import pipeline.

The ordering step matters more than it looks. spring-boot-autoconfigure ships around 150 auto-configuration classes, and several depend on each other — the JPA configuration wants a DataSource to already exist. That is expressed with @AutoConfigureAfter, not with bean dependencies.

The @Conditional family

Every auto-configuration class is guarded. These are the conditions you will actually encounter:

AnnotationFires whenTypical use
@ConditionalOnClassA class is on the classpath"Only configure Redis if Lettuce is present"
@ConditionalOnMissingClassA class is absentChoose between two competing libraries
@ConditionalOnBeanA bean of a type already existsConfigure something that decorates it
@ConditionalOnMissingBeanNo such bean existsThe back-off / override hook
@ConditionalOnPropertyA property has a given valueFeature toggles
@ConditionalOnResourceA classpath resource existsLegacy XML detection
@ConditionalOnWebApplicationServlet or reactive contextWeb-only beans
@ConditionalOnExpressionA SpEL expression is trueCompound conditions

@ConditionalOnClass is evaluated using ASM metadata reading, not by loading the class, so a missing class does not throw NoClassDefFoundError during evaluation. That is why an auto-configuration can safely reference a type that is not on the classpath.

RedisRateLimiterAutoConfiguration.java
@AutoConfiguration(after = RedisAutoConfiguration.class)
@ConditionalOnClass(StringRedisTemplate.class)
@ConditionalOnProperty(prefix = "app.rate-limit", name = "enabled", havingValue = "true")
@EnableConfigurationProperties(RateLimitProperties.class)
public class RedisRateLimiterAutoConfiguration {
 
    @Bean
    @ConditionalOnMissingBean
    public RateLimiter rateLimiter(StringRedisTemplate redis, RateLimitProperties props) {
        return new RedisSlidingWindowRateLimiter(redis, props.limit(), props.window());
    }
}

Read that class as a sentence: after Redis has been configured, if Lettuce is on the classpath, if the feature is switched on, and if the application has not already defined a RateLimiter, register this one.

Why your bean wins

This is the single most useful fact about auto-configuration: user configuration is processed first. @Configuration classes found by component scanning are registered as bean definitions before the import selector contributes its list. By the time @ConditionalOnMissingBean on the auto-configuration is evaluated, your definition is already in the registry, the condition returns false, and Boot backs off silently.

The corollary is the most common gotcha in the framework: @ConditionalOnMissingBean compares bean types, and it only sees definitions that exist at evaluation time. A bean created inside another auto-configuration that happens to be ordered later will not suppress an earlier one. When two starters fight over the same type, the fix is ordering, not conditions.

Debugging with the condition evaluation report

Start the application with --debug (or set debug=true in properties) and Boot prints a report with three sections: positive matches, negative matches, and exclusions.

terminal
./mvnw spring-boot:run -Dspring-boot.run.arguments=--debug
condition evaluation report (excerpt)
Positive matches:
-----------------
   DataSourceAutoConfiguration matched:
      - @ConditionalOnClass found required classes 'javax.sql.DataSource' (OnClassCondition)
 
Negative matches:
-----------------
   RedisRepositoriesAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required class
           'org.springframework.data.redis.repository.configuration.EnableRedisRepositories'

Negative matches are the section worth reading. "Why is there no RedisTemplate?" is answered directly: the class is missing, so add the starter. The same report is exposed at runtime through the Actuator endpoint /actuator/conditions, which is often easier to read in a deployed environment.

To exclude an auto-configuration entirely:

Application.java
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
public class Application { }

or, without touching code, spring.autoconfigure.exclude in properties.

Writing a starter that behaves

Community starters have a bad habit of forcing themselves on the application. A well-behaved starter follows four rules:

  1. Split the modules. acme-spring-boot-autoconfigure holds the configuration classes and has optional/provided dependencies. acme-spring-boot-starter contains no code at all — it exists only to pull in the autoconfigure module plus the libraries it needs.
  2. Guard every bean with @ConditionalOnMissingBean. Assume the application wants to override everything.
  3. Bind configuration through @ConfigurationProperties, not @Value, so the properties appear in IDE completion and can be validated.
  4. Never use @ComponentScan in an auto-configuration. It will scan the user's packages and register beans they never asked for.

The registration file is plain text, one class per line:

src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.acme.ratelimit.RedisRateLimiterAutoConfiguration
com.acme.audit.AuditAutoConfiguration

Add spring-boot-configuration-processor as an optional dependency and your @ConfigurationProperties class generates META-INF/spring-configuration-metadata.json at compile time, which is what gives users autocomplete for app.rate-limit.limit in their IDE.

Ordering between auto-configurations

Three annotations control relative order, and they are placed on the auto-configuration class:

  • @AutoConfigureBefore(X.class) — run before X evaluates.
  • @AutoConfigureAfter(X.class) — run after X, the common case when you need its beans.
  • @AutoConfigureOrder(n) — a coarse numeric hint, rarely needed.

In Boot 3 the @AutoConfiguration annotation takes before and after attributes directly, which is the preferred form. Note that this ordering is about configuration class evaluation, not bean instantiation — bean creation order is still driven by dependencies.

A user-defined DataSource suppresses the auto-configured one while later auto-configurations still see it.

What to take away

Auto-configuration is a filter over a fixed list. When something is missing, the report tells you which condition failed; when something unwanted appears, the fix is either a dependency exclusion or an explicit bean of your own. Neither requires guessing.

The practical habit worth forming: whenever you find yourself surprised by a bean, run with --debug before changing any code. The answer is almost always in the negative matches section, and it takes ten seconds to find.

Frequently Asked Questions

Why does my bean not override the auto-configured one?
Auto-configuration classes are processed after your own configuration, and most of them are guarded by @ConditionalOnMissingBean. If your bean is not winning, it is almost always because it is not being registered at all — a missing @ComponentScan path, a wrong profile, or a @Conditional on your own class that evaluates false. Run the app with --debug and read the negative matches section.
What replaced spring.factories in Spring Boot 3?
Auto-configuration classes are now listed in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports, one fully-qualified class name per line. spring.factories still works for other entry types but is deprecated for auto-configuration and is ignored in Boot 3 for that key.
Is auto-configuration slow?
Condition evaluation is cheap — it is mostly classpath checks that short-circuit early. What costs startup time is instantiating the beans that survive the conditions. If startup matters, exclude the auto-configurations you do not use rather than trying to make evaluation faster.

Related tutorials