How Spring Boot Auto-Configuration Works — The Magic Explained

Spring Boot “just works” — you add a dependency and things appear in your application context. This article explains exactly how that happens, so you can debug it when it doesn’t, and extend it when you need to.

What Auto-Configuration Actually Does

When you add spring-boot-starter-data-jpa to your project, you don’t write a single line of config — yet Spring creates a DataSource, EntityManagerFactory, and JpaTransactionManager automatically. That’s auto-configuration.

Auto-configuration is a set of @Configuration classes that Spring Boot ships. Each one is guarded by conditions — it only activates if certain classes are on the classpath, certain beans are missing, certain properties are set, or certain files exist.

The net effect: your app gets sensible defaults, and you override only what you need.

The Entry Point: @SpringBootApplication

@SpringBootApplication
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

@SpringBootApplication is a composed annotation that includes:

@SpringBootConfiguration   // marks this as a configuration class
@EnableAutoConfiguration   // turns on auto-configuration
@ComponentScan             // scans the package and sub-packages

@EnableAutoConfiguration is the key. It triggers the auto-configuration machinery.

How Spring Discovers Auto-Configuration Classes

Spring Boot 3+ uses a file called:

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

(In Spring Boot 2 it was META-INF/spring.factories — same idea, different format.)

Every Spring Boot starter JAR ships this file listing its auto-configuration classes. When @EnableAutoConfiguration activates, Spring Boot reads all these files from every JAR on the classpath and collects the full list of candidate configurations.

You can see all of them:

# List every auto-configuration class loaded
./mvnw spring-boot:run \
  -Dspring-boot.run.jvmArguments="-Ddebug"

Or look in the actuator:

GET /actuator/conditions

The response shows every auto-configuration and whether it matched or was skipped — and why.

The @Conditional System

Auto-configuration classes don’t blindly apply. Every one is wrapped in conditions. Spring Boot ships a rich set:

AnnotationCondition
@ConditionalOnClassA class must be on the classpath
@ConditionalOnMissingClassA class must NOT be on the classpath
@ConditionalOnBeanA bean of a certain type must exist
@ConditionalOnMissingBeanA bean of a certain type must NOT exist
@ConditionalOnPropertyA property must be set (or have a specific value)
@ConditionalOnResourceA file must exist on the classpath
@ConditionalOnWebApplicationApp must be a web application
@ConditionalOnExpressionA SpEL expression must evaluate to true

A Real Example: DataSourceAutoConfiguration

Here’s a simplified version of how Spring Boot auto-configures a DataSource:

@AutoConfiguration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class,
          DataSourceCheckpointRestoreConfiguration.class })
public class DataSourceAutoConfiguration {

    @Configuration(proxyBeanMethods = false)
    @Conditional(EmbeddedDatabaseCondition.class)
    @ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
    @Import(EmbeddedDataSourceConfiguration.class)
    protected static class EmbeddedDatabaseConfiguration {
    }

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
    @ConditionalOnSingleCandidate(DataSourceProperties.class)
    protected static class PooledDataSourceConfiguration {

        @Bean
        @ConditionalOnMissingBean
        public DataSource dataSource(DataSourceProperties properties) {
            // creates HikariCP pool with properties from application.properties
        }
    }
}

What this says:

  1. Only activate if DataSource class is on the classpath (@ConditionalOnClass)
  2. If there’s already a DataSource bean — back off (@ConditionalOnMissingBean)
  3. Read config from spring.datasource.* properties
  4. Create the DataSource bean

This is the fundamental pattern: auto-configuration backs off whenever you provide your own bean.

The Request Flow Through Auto-Configuration

Application starts
       │
       ▼
@SpringBootApplication found
       │
       ▼
@EnableAutoConfiguration activates
       │
       ▼
Spring reads all AutoConfiguration.imports files from classpath JARs
       │
       ▼
Collects ~150 candidate auto-configuration classes
       │
       ▼
For each candidate:
  - Evaluate @Conditional annotations
  - If all conditions pass → apply the @Configuration
  - If any condition fails → skip (logged in /actuator/conditions)
       │
       ▼
Beans from active configurations are added to ApplicationContext
       │
       ▼
Your @Component/@Service/@Repository beans are registered
       │
       ▼
Application ready

Seeing It in Action

Add the Actuator and hit the conditions endpoint:

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
# application.properties
management.endpoints.web.exposure.include=conditions,beans
# See all conditions decisions
curl http://localhost:8080/actuator/conditions | jq .

# See all beans in the context
curl http://localhost:8080/actuator/beans | jq .

The conditions report has two sections:

  • positiveMatches — configurations that were applied and why
  • negativeMatches — configurations that were skipped and which condition failed

Example: Why DataSource was auto-configured

{
  "positiveMatches": {
    "DataSourceAutoConfiguration": [
      {
        "condition": "OnClassCondition",
        "message": "@ConditionalOnClass found required classes 'javax.sql.DataSource', 'org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType'"
      }
    ]
  }
}

Example: Why WebMvcAutoConfiguration was skipped (in a non-web app)

{
  "negativeMatches": {
    "WebMvcAutoConfiguration": {
      "notMatched": [
        {
          "condition": "OnWebApplicationCondition",
          "message": "@ConditionalOnWebApplication did not find reactive or servlet-based web application"
        }
      ]
    }
  }
}

Overriding Auto-Configuration

There are three ways to override:

1. Define your own bean

The most common. Auto-configuration uses @ConditionalOnMissingBean, so your bean wins:

@Configuration
public class DataSourceConfig {

    @Bean
    public DataSource dataSource() {
        // Your custom DataSource — auto-config backs off
        HikariDataSource ds = new HikariDataSource();
        ds.setJdbcUrl("jdbc:postgresql://localhost:5432/orders");
        ds.setUsername("app");
        ds.setPassword("secret");
        ds.setMaximumPoolSize(20);
        return ds;
    }
}

2. Set properties

Most auto-configurations are driven by properties. You don’t need a custom bean — just configure:

# application.yml
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/orders
    username: app
    password: secret
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000

3. Exclude an auto-configuration entirely

If you want to disable an auto-configuration class:

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

Or via properties:

spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration

Writing Your Own Auto-Configuration

You can write custom auto-configuration for your own libraries or shared components.

Step 1: Write a @AutoConfiguration class:

@AutoConfiguration
@ConditionalOnClass(OrderRepository.class)
@ConditionalOnMissingBean(OrderService.class)
@EnableConfigurationProperties(OrderServiceProperties.class)
public class OrderServiceAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public OrderService orderService(OrderRepository repository,
                                    OrderServiceProperties props) {
        return new OrderService(repository, props.getMaxItemsPerOrder());
    }
}

Step 2: Create the properties class:

@ConfigurationProperties(prefix = "order")
public class OrderServiceProperties {
    private int maxItemsPerOrder = 50;

    public int getMaxItemsPerOrder() { return maxItemsPerOrder; }
    public void setMaxItemsPerOrder(int n) { this.maxItemsPerOrder = n; }
}

Step 3: Register it in src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:

com.devopsmonk.order.autoconfigure.OrderServiceAutoConfiguration

Step 4: Any application that includes your JAR gets OrderService auto-wired — unless they define their own.

The @ConfigurationProperties Pattern

Auto-configurations bind external properties to typed beans using @ConfigurationProperties. You should use this for your own configuration too — it’s far better than injecting individual @Value fields.

@ConfigurationProperties(prefix = "app.order")
@Validated
public class AppOrderProperties {

    @NotNull
    private String defaultCurrency = "USD";

    @Min(1) @Max(1000)
    private int maxItemsPerOrder = 50;

    private Duration processingTimeout = Duration.ofSeconds(30);

    // getters and setters
}

Enable it in your main configuration:

@SpringBootApplication
@EnableConfigurationProperties(AppOrderProperties.class)
public class OrderServiceApplication { ... }

Or annotate the properties class with @Component directly.

Now configure it in application.yml:

app:
  order:
    default-currency: EUR
    max-items-per-order: 100
    processing-timeout: 60s

Spring Boot will validate these at startup and give a clear error if they’re wrong.

Common Debugging Scenarios

Problem: My DataSource isn’t being created.

# Enable debug logging
logging.level.org.springframework.boot.autoconfigure=DEBUG

Check the conditions report. Look for DataSourceAutoConfiguration in negativeMatches. The reason will tell you exactly which condition failed.

Problem: I defined a DataSource bean but there are now two.

Check if your bean is defined and the auto-configuration is also active. Auto-config should back off via @ConditionalOnMissingBean. If it didn’t, you may have defined the bean conditionally yourself, and auto-config evaluated before your bean was registered.

Problem: I want auto-configuration for a profile-specific bean.

Use @Profile alongside @Bean:

@Bean
@Profile("prod")
public DataSource prodDataSource() { ... }

Auto-configuration’s @ConditionalOnMissingBean is context-aware — it will see your profile-activated bean only if that profile is active.

What You’ve Learned

  • @SpringBootApplication includes @EnableAutoConfiguration
  • Spring Boot discovers auto-configuration classes via AutoConfiguration.imports in each JAR
  • Every auto-configuration is guarded by @Conditional annotations — it backs off if conditions aren’t met
  • The most important condition is @ConditionalOnMissingBean — define your own bean and auto-config steps aside
  • Use /actuator/conditions to see exactly why each configuration was applied or skipped
  • Write your own auto-configuration using the same pattern

Next: Article 5 — Application Configuration: Properties, YAML, and Profiles — how to configure your app for different environments without changing code.