Ch. 8 · Spring Boot

Spring Boot Auto-Configuration and Explicit Overrides

How Spring Boot auto-configuration works: AutoConfiguration.imports, @ConditionalOnMissingBean, the conditions report, and safe overrides.

~6 min readbeginnerupdated Oct 6, 2026

“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;
    }
}
java

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.SimpleJacksonAutoConfiguration
Text

The 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_null
yaml

The 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);
    }
}
java

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();
    }
}
java

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)
Text

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 { }
java
spring:
  autoconfigure:
    exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
yaml

Exclusion 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
    }
}
java

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.factories registration for auto-configuration is no longer supported.
  • Using @ConditionalOnMissingBean in 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));
    }
}
java

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.

Continue learning

More in Spring Boot

read ✓Spring Boot · easy

Spring vs Spring Boot: What Is the Difference?

Spring Framework provides dependency injection, MVC and transactions; Spring Boot sets it up with starters, auto-configuration and a server.

~6 min readread →
esc