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.
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
@Configurationclasses listed in a file, filtered by conditions. @ConditionalOnMissingBeanis 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.
--debugprints 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.
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:
| Annotation | Fires when | Typical use |
|---|---|---|
@ConditionalOnClass | A class is on the classpath | "Only configure Redis if Lettuce is present" |
@ConditionalOnMissingClass | A class is absent | Choose between two competing libraries |
@ConditionalOnBean | A bean of a type already exists | Configure something that decorates it |
@ConditionalOnMissingBean | No such bean exists | The back-off / override hook |
@ConditionalOnProperty | A property has a given value | Feature toggles |
@ConditionalOnResource | A classpath resource exists | Legacy XML detection |
@ConditionalOnWebApplication | Servlet or reactive context | Web-only beans |
@ConditionalOnExpression | A SpEL expression is true | Compound 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.
@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.
./mvnw spring-boot:run -Dspring-boot.run.arguments=--debugPositive 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:
@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:
- Split the modules.
acme-spring-boot-autoconfigureholds the configuration classes and hasoptional/provideddependencies.acme-spring-boot-startercontains no code at all — it exists only to pull in the autoconfigure module plus the libraries it needs. - Guard every bean with
@ConditionalOnMissingBean. Assume the application wants to override everything. - Bind configuration through
@ConfigurationProperties, not@Value, so the properties appear in IDE completion and can be validated. - Never use
@ComponentScanin 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:
com.acme.ratelimit.RedisRateLimiterAutoConfiguration
com.acme.audit.AuditAutoConfigurationAdd 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.
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?
What replaced spring.factories in Spring Boot 3?
Is auto-configuration slow?
Related tutorials
- Spring IoC Container InternalsThe Spring container from the inside: the full bean lifecycle in order, what BeanPostProcessor actually intercepts, every bean scope, and how circular dependencies are resolved.
- Configuration & Profiles MasteryType-safe configuration with @ConfigurationProperties, the full property precedence order, relaxed binding rules, profile groups, and keeping secrets out of your YAML.
- Spring AOP & Aspect-Oriented ProgrammingSpring AOP from pointcut syntax to proxy mechanics: the five advice types, writing annotation-driven aspects, aspect ordering, and why self-invocation silently does nothing.
- Actuator & Observability EndpointsEvery Actuator endpoint worth exposing, writing custom health indicators for Kubernetes probes, adding Micrometer metrics that answer real questions, and securing it all.