How Spring Boot Auto-Configuration Actually Works (Behind the Magic)

“Spring Boot is magic” is something you hear a lot. Add spring-boot-starter-data-jpa and suddenly you have a working DataSource, a JpaTransactionManager, and a LocalContainerEntityManagerFactoryBean — without writing a single @Bean method. Understanding how this actually works turns the magic into a tool you can control, debug, and extend.


The Entry Point: @EnableAutoConfiguration

@SpringBootApplication is a shorthand for three annotations:

@Configuration
@EnableAutoConfiguration   // this is the one that matters here
@ComponentScan
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

@EnableAutoConfiguration tells Spring to scan for auto-configuration classes and activate the ones that match the current application context.


How Spring Finds Auto-Configurations

Auto-configuration classes are registered in a file inside each JAR:

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

Spring Boot reads this file from every JAR on the classpath at startup. Each line is a fully-qualified auto-configuration class name:

org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
# ... hundreds more

In Spring Boot 3.x, all of these were in a single spring-boot-autoconfigure JAR. In Spring Boot 4, this was split into 70+ individual modules — each starter pulls in only the modules it needs, reducing startup processing time significantly.

flowchart TD
    App[SpringApplication.run] --> Scan[Scan AutoConfiguration.imports\nin all JARs on classpath]
    Scan --> List[Load list of candidate\nauto-configuration classes]
    List --> Filter[Evaluate @Conditional\nannotations on each class]
    Filter --> Active[Activate matching configurations]
    Filter --> Skip[Skip non-matching ones]
    Active --> Beans[Register beans into\nApplication Context]

Conditional Annotations: The Core Mechanism

Every auto-configuration class is guarded by one or more @Conditional annotations. These evaluate at startup and determine whether the configuration class is activated.

@ConditionalOnClass

Activates only if a specific class is present on the classpath:

@AutoConfiguration
@ConditionalOnClass(DataSource.class)  // only if DataSource is on the classpath
public class DataSourceAutoConfiguration {
    // This entire class is skipped if you have no JDBC starter
}

This is how spring-boot-starter-data-jpa triggers JPA auto-configuration: the JPA JAR puts EntityManager on the classpath, which satisfies @ConditionalOnClass(EntityManager.class).

@ConditionalOnMissingBean

Activates only if you have NOT defined a bean of that type yourself:

@AutoConfiguration
@ConditionalOnClass(DataSource.class)
public class DataSourceAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(DataSource.class)  // backs off if you define your own
    public DataSource dataSource(DataSourceProperties properties) {
        return DataSourceBuilder.create()
            .url(properties.getUrl())
            .username(properties.getUsername())
            .password(properties.getPassword())
            .build();
    }
}

This is the non-invasive principle: your explicit @Bean definitions always take precedence over auto-configuration. Define your own DataSource bean and the auto-configured one never gets created.

@ConditionalOnProperty

Activates only if a property is set to a specific value:

@Bean
@ConditionalOnProperty(
    name = "spring.cache.type",
    havingValue = "redis",
    matchIfMissing = false
)
public CacheManager redisCacheManager(RedisConnectionFactory factory) {
    return RedisCacheManager.create(factory);
}

matchIfMissing = true means: activate even if the property is not set (i.e., use this as the default). matchIfMissing = false means: only activate when the property is explicitly set.

@ConditionalOnBean

The inverse of @ConditionalOnMissingBean — activates only if another bean already exists:

@Bean
@ConditionalOnBean(DataSource.class)  // only create if there is a DataSource
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
    return new JdbcTemplate(dataSource);
}

Other Conditionals

AnnotationCondition
@ConditionalOnWebApplicationRunning in a web context
@ConditionalOnNotWebApplicationNot running in a web context
@ConditionalOnResourceA specific file resource exists
@ConditionalOnExpressionA SpEL expression evaluates to true
@ConditionalOnSingleCandidateExactly one candidate bean exists
@ConditionalOnJavaA specific Java version range
@ConditionalOnCloudPlatformRunning on a specific cloud (Kubernetes, Heroku, etc.)

Debugging: The Conditions Evaluation Report

When auto-configuration behaves unexpectedly — a bean is not being created, or the wrong one is — Spring Boot has a built-in diagnostic tool.

Start your application with --debug:

java -jar app.jar --debug

This prints a full CONDITIONS EVALUATION REPORT that shows every auto-configuration class, whether it was activated or not, and exactly why:

============================
CONDITIONS EVALUATION REPORT
============================

Positive matches:   (configurations that DID activate)
-----------------
   DataSourceAutoConfiguration matched:
      - @ConditionalOnClass found required class 'javax.sql.DataSource' (OnClassCondition)

   HibernateJpaAutoConfiguration matched:
      - @ConditionalOnClass found required classes 'javax.persistence.EntityManager',
        'org.hibernate.engine.spi.SessionImplementor' (OnClassCondition)
      - @ConditionalOnBean (types: javax.sql.DataSource) found bean 'dataSource' (OnBeanCondition)

Negative matches:   (configurations that DID NOT activate, and why)
-----------------
   ActiveMQAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required class
           'javax.jms.ConnectionFactory' (OnClassCondition)

   MongoAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required class
           'com.mongodb.client.MongoClient' (OnClassCondition)

This tells you exactly which condition caused each decision. If a bean you expect is missing, check the negative matches section first.

In the IDE: Set the logging.level.org.springframework.boot.autoconfigure to DEBUG in application.properties for the same output during development.


Excluding Auto-Configurations

To disable a specific auto-configuration:

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

Or via properties (useful for environment-specific exclusions):

spring:
  autoconfigure:
    exclude:
      - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
      - org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration

Common reasons to exclude:

  • Testing without a database (exclude DataSourceAutoConfiguration)
  • Replacing Spring Security with a custom auth mechanism
  • Replacing the default ObjectMapper configuration

Configuration Properties: How Auto-Config Reads Your Settings

Auto-configuration classes read your application.yml / application.properties via @ConfigurationProperties beans. These are type-safe property bindings:

// Spring Boot reads spring.datasource.* properties into this class
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties {
    private String url;
    private String username;
    private String password;
    private String driverClassName;
    // getters and setters
}

You can write your own @ConfigurationProperties to bind your custom configuration in the same way:

@ConfigurationProperties(prefix = "myapp.cache")
@Validated
public class CacheProperties {

    @NotNull
    private Duration ttl = Duration.ofMinutes(10);

    private int maxSize = 1000;

    private boolean enabled = true;

    // getters and setters
}
myapp:
  cache:
    ttl: 30m
    max-size: 5000
    enabled: true

Register it as a bean:

@Configuration
@EnableConfigurationProperties(CacheProperties.class)
public class CacheConfig { ... }

Or simply annotate it with @Component.


Writing Your Own Auto-Configuration

This is useful for internal libraries shared across multiple Spring Boot applications — instead of every app configuring the same beans, you write it once as a starter.

Step 1: The Auto-Configuration Class

package com.example.mylib.autoconfigure;

@AutoConfiguration
@ConditionalOnClass(MyServiceClient.class)
@EnableConfigurationProperties(MyServiceProperties.class)
public class MyServiceAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean  // allows apps to override with their own bean
    public MyServiceClient myServiceClient(MyServiceProperties props) {
        return new MyServiceClient(props.getBaseUrl(), props.getApiKey());
    }
}

Step 2: The Properties Class

@ConfigurationProperties(prefix = "myservice")
public class MyServiceProperties {

    @NotEmpty
    private String baseUrl;

    private String apiKey;

    // getters and setters
}

Step 3: Register the Auto-Configuration

Create the registration file in your library JAR:

src/main/resources/META-INF/spring/
  org.springframework.boot.autoconfigure.AutoConfiguration.imports

Contents:

com.example.mylib.autoconfigure.MyServiceAutoConfiguration

Create a separate my-service-spring-boot-starter module that just has dependencies:

<dependencies>
    <!-- Your auto-configuration module -->
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>my-service-autoconfigure</artifactId>
        <version>${project.version}</version>
    </dependency>
    <!-- The actual library being configured -->
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>my-service-client</artifactId>
        <version>${project.version}</version>
    </dependency>
</dependencies>

Any Spring Boot app that adds my-service-spring-boot-starter now automatically gets a configured MyServiceClient bean — no manual @Bean method needed — and can override it by defining their own.


Auto-Configuration Order and Dependencies

Sometimes one auto-configuration depends on another. Use @AutoConfigureAfter and @AutoConfigureBefore to control ordering:

@AutoConfiguration
@AutoConfigureAfter(DataSourceAutoConfiguration.class)  // run after DataSource is configured
public class FlywayAutoConfiguration {

    @Bean
    @ConditionalOnBean(DataSource.class)
    public Flyway flyway(DataSource dataSource) {
        return Flyway.configure().dataSource(dataSource).load();
    }
}

Spring Boot 4: Modularisation Impact

In Spring Boot 4, the monolithic spring-boot-autoconfigure JAR was split into modules. The key change for custom starter authors: you now reference module-specific JARs instead of the monolithic one.

<!-- Spring Boot 3.x custom starter dependency -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
    <!-- includes ALL auto-configs — you get the whole thing -->
</dependency>

<!-- Spring Boot 4.x custom starter — reference only what you need -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure-jdbc</artifactId>
    <!-- only the JDBC auto-config module -->
</dependency>

If your custom auto-configuration extends or conditionally depends on a built-in auto-config class, check that the specific module JARs are on the classpath, not just the monolithic one.


Common Auto-Configuration Pitfalls

1. Defining a bean that shadows auto-configuration but with the wrong type

@Bean
public DataSource dataSource() {
    // This backs off DataSourceAutoConfiguration — as intended
    return new HikariDataSource(config);
}

// But if you accidentally use the wrong type:
@Bean
public javax.sql.DataSource dataSource() {  // interface vs impl
    // This might NOT trigger @ConditionalOnMissingBean(DataSource.class)
    // depending on how the condition resolves the type
}

Be explicit: match the type in your @Bean method to the type in @ConditionalOnMissingBean.

2. Auto-configuration in the wrong package

Auto-configuration classes must NOT be in the main application package (or a subpackage). Component scan would pick them up twice — once through @ComponentScan and once through auto-configuration. Put custom auto-configuration in a separate JAR.

3. Forgetting @AutoConfiguration (using @Configuration instead)

@Configuration classes found by component scan are processed differently from auto-configuration classes. Auto-configuration classes should use @AutoConfiguration (Spring Boot 2.7+), which is a specialised form of @Configuration that participates correctly in the ordering and exclusion mechanisms.


Quick Reference

GoalHow
See what auto-configuredjava -jar app.jar --debug → CONDITIONS EVALUATION REPORT
Disable a specific auto-config@SpringBootApplication(exclude = {...}) or spring.autoconfigure.exclude
Override an auto-configured beanDefine your own @Bean of the same type
Custom auto-configuration@AutoConfiguration class + AutoConfiguration.imports registration file
Type-safe config properties@ConfigurationProperties(prefix = "...")
Control activation@ConditionalOnClass, @ConditionalOnMissingBean, @ConditionalOnProperty
Control order@AutoConfigureAfter, @AutoConfigureBefore

Auto-configuration is not magic — it is a well-structured conditional bean registration system. Once you understand conditions, the registration file, and the non-invasive override principle, you can predict exactly what Spring Boot will and will not create for any combination of dependencies and properties.

Abhay

Abhay Pratap Singh

DevOps Engineer passionate about automation, cloud infrastructure, and self-hosted tools. I write about Kubernetes, Terraform, DNS, and everything in between.