“How does Spring Boot auto-configuration work?” is one of the most common Spring Boot interview questions, and “it configures things automatically” is the answer that ends the conversation badly. Interviewers want to hear how Boot decides, when it backs off, and how you would find out why a bean did or did not appear. That last part matters in real work: half of all “why is Boot ignoring my config” tickets are answered by the conditions report.
Before you start
You should know what a Spring bean is, what @Configuration and @Bean do, and how component scanning finds classes under your main application package. The examples use Spring Boot 3.x, where auto-configuration registration moved from spring.factories to a dedicated imports file.
The short answer
@SpringBootApplication includes @EnableAutoConfiguration, which loads a list of auto-configuration classes from META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports in every jar on the classpath. Each class is guarded by conditions, mainly @ConditionalOnClass (is the library present?), @ConditionalOnProperty (is it switched on?) and @ConditionalOnMissingBean (has the user already defined one?). Auto-configuration runs after your own configuration is registered, so defining your own bean makes Boot’s default back off. You inspect the decisions with --debug or the conditions actuator endpoint.
How it works
Here is a simplified version of what an auto-configuration class looks like. Real ones in spring-boot-autoconfigure follow the same pattern.
@AutoConfiguration
@ConditionalOnClass(ObjectMapper.class) // only if Jackson is on the classpath
@EnableConfigurationProperties(JacksonProperties.class)
public class SimpleJacksonAutoConfiguration {
@Bean
@ConditionalOnMissingBean // back off if the app defines its own
ObjectMapper objectMapper(JacksonProperties props) {
ObjectMapper mapper = new ObjectMapper();
// apply spring.jackson.* properties here
return mapper;
}
}And it is registered by a plain text file, one class name per line:
# src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.autoconfigure.SimpleJacksonAutoConfigurationThe ordering is the key. Spring first processes your @Configuration classes and scanned components. Only then does the AutoConfigurationImportSelector (a deferred import selector) process auto-configurations. By the time @ConditionalOnMissingBean is evaluated, your beans are already registered as definitions, so the condition can see them. This is also why @ConditionalOnMissingBean is documented as reliable only inside auto-configuration classes: in your own configuration the order is not guaranteed.
Auto-configurations can order themselves relative to each other with @AutoConfiguration(after = DataSourceAutoConfiguration.class), which is how JPA configuration waits for the DataSource.
Step-by-step walkthrough
Step 1: Add a starter and observe the default
Add spring-boot-starter-web. Jackson arrives transitively, JacksonAutoConfiguration sees ObjectMapper on the classpath, finds no user-defined mapper, and creates one configured from spring.jackson.* properties. Your controllers serialize JSON without any code.
spring:
jackson:
default-property-inclusion: non_nullThe first lever is properties. Most customization should stop here because you keep all of Boot’s other defaults.
Step 2: Customize without replacing
Many auto-configurations expose customizer callbacks that adjust Boot’s bean rather than replacing it:
@Configuration
class JsonConfig {
@Bean
Jackson2ObjectMapperBuilderCustomizer javaTimeAsIso() {
return builder -> builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
}Customizers compose: several libraries can each contribute one, and Boot still owns the bean.
Step 3: Replace the bean entirely
If you define your own ObjectMapper, @ConditionalOnMissingBean sees it and Boot creates nothing.
@Configuration
class JsonConfig {
@Bean
ObjectMapper objectMapper() {
return JsonMapper.builder().findAndAddModules().build();
}
}This is powerful and dangerous. Your mapper does not receive spring.jackson.* properties or the customizers from Step 2, because those were wired into the bean you replaced.
Step 4: Read the conditions report
Start the app with --debug (or debug=true in properties). Boot prints a report grouped into positive matches, negative matches, exclusions and unconditional classes.
============================
CONDITIONS EVALUATION REPORT
============================
Positive matches:
-----------------
DataSourceAutoConfiguration matched:
- @ConditionalOnClass found required classes 'javax.sql.DataSource', ... (OnClassCondition)
Negative matches:
-----------------
JacksonAutoConfiguration.JacksonObjectMapperConfiguration#jacksonObjectMapper:
Did not match:
- @ConditionalOnMissingBean (types: com.fasterxml.jackson.databind.ObjectMapper; SearchStrategy: all)
found beans of type 'com.fasterxml.jackson.databind.ObjectMapper' objectMapper (OnBeanCondition)That negative match tells you exactly which of your beans caused the back-off. With Actuator, /actuator/conditions returns the same data as JSON.
Step 5: Exclude when you must
To turn off an entire auto-configuration, exclude it by class or property:
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class App { }spring:
autoconfigure:
exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfigurationExclusion is a blunt tool. Prefer it for things you genuinely do not want, such as a library that drags in a DataSource dependency for a service with no database (see the failed to configure a DataSource guide).
Worked scenario
A team adds a custom ObjectMapper bean to register a money module. Within a day, API responses change: null fields reappear and dates are serialized as numbers. Nobody touched application.yml, which still says default-property-inclusion: non_null.
The conditions report shows jacksonObjectMapper in the negative matches because the custom mapper exists. Boot’s mapper, the one that applied spring.jackson.*, was never created. The property is now ignored, and the JavaTimeModule registration that Boot handled has gone too.
The fix is to customize instead of replace:
@Configuration
class JsonConfig {
@Bean
Module moneyModule() {
return new MoneyModule(); // any com.fasterxml.jackson.databind.Module bean is registered by Boot
}
}Boot’s Jackson auto-configuration registers every Module bean it finds, so the custom module is added and all properties keep working. A wider lesson: before you define a bean of a type Boot configures, check whether a property, customizer or module bean already gives you the hook.
Common mistake
- “Auto-configuration uses component scanning.” It does not. Auto-configuration classes live in libraries and are loaded from the imports file. Placing them under your scanned package actually breaks ordering.
- “Boot uses spring.factories.” That was Spring Boot 2. Boot 3 reads
AutoConfiguration.imports;spring.factoriesregistration for auto-configuration is no longer supported. - Using
@ConditionalOnMissingBeanin ordinary application configuration and expecting deterministic results. Its behavior depends on registration order, which is only defined for auto-configuration. - Replacing a bean to change one setting, losing every property and customizer in the process.
- Excluding auto-configuration to fix an error you have not read. The conditions report and the failure analyzer usually point to the real cause.
Verify the behavior
ApplicationContextRunner tests auto-configuration without starting a server. It is the same tool the Spring Boot team uses.
class SimpleJacksonAutoConfigurationTest {
private final ApplicationContextRunner runner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(SimpleJacksonAutoConfiguration.class));
@Test
void createsMapperByDefault() {
runner.run(ctx -> assertThat(ctx).hasSingleBean(ObjectMapper.class));
}
@Test
void backsOffWhenUserDefinesOne() {
runner.withBean("custom", ObjectMapper.class, ObjectMapper::new)
.run(ctx -> assertThat(ctx).hasSingleBean(ObjectMapper.class)
.hasBean("custom"));
}
@Test
void skipsWhenJacksonMissing() {
runner.withClassLoader(new FilteredClassLoader(ObjectMapper.class))
.run(ctx -> assertThat(ctx).doesNotHaveBean(ObjectMapper.class));
}
}Follow-up questions
How would you write your own starter? Put the @AutoConfiguration class and the imports file in an -autoconfigure module, guard every bean with conditions, bind settings through @ConfigurationProperties, and provide a -starter module that only pulls dependencies.
What is @ConditionalOnBean and why is it risky? It creates a bean only if another bean exists. It is order sensitive; use it in auto-configuration with after = ordering, not in user configuration.
What does proxyBeanMethods = false do on @AutoConfiguration? It is the default for @AutoConfiguration. Boot does not CGLIB-proxy the class, so calling one @Bean method from another creates a new instance; inject dependencies as method parameters instead.
How do you see which auto-configurations are active at runtime? /actuator/conditions, or the startup report with --debug.
Interview exercise
Your service connects to two databases. You define two DataSource beans and suddenly Spring Data JPA fails to start, and so does Flyway. Explain what happened in terms of auto-configuration and how you would fix it.
Answer and reasoning
DataSourceAutoConfiguration backed off because @ConditionalOnMissingBean(DataSource.class) found your beans, which is expected. JPA and Flyway auto-configuration, however, need a single DataSource to inject; with two candidates and no @Primary, they hit “expected single matching bean but found 2” (see no qualifying bean). Mark one DataSource as @Primary so Boot’s JPA and Flyway configurations use it, and configure the second EntityManagerFactory and transaction manager explicitly for the other database. The reasoning to show: you know Boot backs off by type, you read the conditions report to confirm, and you resolve ambiguity deliberately rather than excluding auto-configuration.