← Back to Blog

Spring Boot Auto-Configuration Is Magic Until It Isn't

Spring Boot configures your app without a line of config. Then it configures something you did not want. Here is how the mechanism works and how to control it.

You add spring-boot-starter-data-redis to your pom.xml. You haven't written a single line of Redis configuration. You haven't created a RedisTemplate bean. You haven't touched application.properties. You restart the app, and Redis is just there. A fully configured RedisTemplate, ready to use. You inject it into a service, write a value, read it back. Works on the first try.

This is the moment Spring Boot is designed for. It feels like magic because it is, in a way. Someone wrote a lot of conditional logic so you wouldn't have to. You added a dependency and the framework figured out the rest.

The problem is that magic only feels good when it does what you expect. When it doesn't, you're debugging behavior you never configured, looking for code that doesn't exist in your project.

How It Actually Works

Auto-configuration isn't magic. It's a list of configuration classes that Spring Boot tries to apply at startup. Each class is annotated with conditions that control whether it activates.

When you build a Spring Boot starter, the auto-configuration classes are registered in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. Spring Boot reads that file at startup and evaluates every class in it. Each class is annotated with conditions like @ConditionalOnClass, @ConditionalOnMissingBean, @ConditionalOnProperty. If all conditions pass, the configuration activates and creates its beans. If any condition fails, the configuration is skipped entirely.

@ConditionalOnClass is the most common one. It says: only activate this configuration if this class is on the classpath. The Jedis auto-configuration activates if JedisConnectionFactory.class is present, which it is, because you added the Redis starter, which bundles Jedis. That's the whole mechanism. Class on the classpath → auto-configuration activates → beans appear in your context.

@ConditionalOnMissingBean is the one you interact with most directly. It says: only create this bean if no bean of this type already exists in the context. This is the escape hatch that lets you override auto-configured beans. Define your own RedisTemplate bean and Spring Boot's auto-configuration will step aside. Your bean takes precedence. In theory.

The Debug Flag You Need to Know

Before you can debug auto-configuration problems, you need to see what's actually happening. There are two ways to do this.

The first is the --debug flag. Start your application with --debug and Spring Boot prints a CONDITIONS EVALUATION REPORT to the console. It's long, hundreds of lines, but it tells you exactly which auto-configuration classes matched, which ones didn't match, and why.

The output has two sections. "Positive matches" shows configurations that activated and why. "Negative matches" shows configurations that didn't activate and what condition failed. If you're wondering why Redis isn't being configured even though you added the starter, the negative matches section will tell you: RedisAutoConfiguration did not match: @ConditionalOnMissingBean(RedisConnectionFactory) found bean 'redisConnectionFactory'. Someone already created a RedisConnectionFactory bean somewhere. The auto-configuration saw it and backed off.

The second option is the Actuator conditions endpoint. If you have spring-boot-starter-actuator and expose the conditions endpoint, hitting /actuator/conditions returns the same report as JSON. Easier to filter, easier to search, and available at runtime without restarting. These two tools answer 90% of auto-configuration questions. Before spending time reading Spring Boot source code, run the debug flag first.

The Override That Did Nothing

The first time I tried to customize an auto-configured bean, I created a @Configuration class with an @Bean method returning a new ObjectMapper with my settings. I injected it into a service, called it directly, everything looked correct. My settings were there. Then I made an HTTP request. The response JSON was formatted with Spring's defaults, not mine.

Spring MVC had a reference to the auto-configured ObjectMapper, not mine. The fix isn't to create a raw ObjectMapper bean. It's to use the extension point the framework provides:

@Bean
public Jackson2ObjectMapperBuilderCustomizer customizer() {
    return builder -> builder
        .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
        .modules(new JavaTimeModule());
}

Spring Boot applies all registered customizers to its ObjectMapper during construction. You get one ObjectMapper, customized the way you want. This pattern exists for most auto-configured beans: DataSourceBuilderCustomizer, WebMvcConfigurer, SecurityFilterChain. The framework expects you to extend through customizers, not replace through raw beans.

When You Add a Second DataSource

We had a primary PostgreSQL database and a secondary read-only replica. I created two DataSource beans, annotated one with @Primary. The application started. No errors. But then I tried to run a JdbcTemplate query and got a NoUniqueBeanDefinitionException.

DataSourceAutoConfiguration uses @ConditionalOnSingleCandidate(DataSource.class), not @ConditionalOnMissingBean. With two DataSources, even if one is @Primary, Spring Boot's auto-configuration for JdbcTemplate backs off entirely. The fix is to exclude the auto-configurations and configure manually:

@SpringBootApplication(exclude = {
    DataSourceAutoConfiguration.class,
    DataSourceTransactionManagerAutoConfiguration.class,
    JdbcTemplateAutoConfiguration.class
})

This feels like a step backward, but it's the honest trade-off. Auto-configuration works well for the common case. Two data sources is not the common case. When you step outside the defaults, configure manually.

The Classpath Trap

The most disorienting form of auto-configuration magic is the kind triggered purely by classpath presence. You add a dependency for something unrelated: a testing library, a utility jar, a starter your teammate recommended. Your application restarts. Something is different. Endpoints that worked before now return 401. Or a database you didn't know was there shows up in your connection pool.

The Spring Security 401 is the most common version. You add spring-boot-starter-security to the classpath, maybe because a library you pulled in depends on it transitively, and suddenly every endpoint requires authentication. You didn't configure anything. But SecurityAutoConfiguration saw its classes on the classpath and activated.

The H2 version is subtler. You add spring-boot-starter-test, which transitively includes H2. Spring Boot sees the H2 driver, sees no configured spring.datasource.url, and decides to create an in-memory database for you. If your local properties file has the real database URL it overrides this. But in an environment where properties weren't applied correctly, you're suddenly writing to an in-memory database that disappears on restart.

The rule: know what's on your classpath. Run mvn dependency:tree | grep starter periodically. Every starter that ends up on your classpath is a potential activation trigger for auto-configuration you haven't thought about.

Exclusions Are Your Escape Hatch

When auto-configuration activates something you don't want, you have two ways to turn it off. The annotation approach:

@SpringBootApplication(exclude = {
    SecurityAutoConfiguration.class,
    UserDetailsServiceAutoConfiguration.class
})

Or the property approach, useful when you don't want to touch the main application class or need environment-specific disabling:

spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration

Commonly excluded: DataSourceAutoConfiguration when managing multiple data sources manually; SecurityAutoConfiguration and UserDetailsServiceAutoConfiguration in services that handle their own authentication; FlywayAutoConfiguration when controlling migration timing explicitly.

What I Do Now

Run with --debug during development. The conditions report is verbose but answers questions before you ask them. If you're setting up a new project or adding a new dependency, run it once and scan the negative matches. You'll catch unexpected behavior before it surprises you in production.

Prefer customizers over bean replacement. Jackson2ObjectMapperBuilderCustomizer, WebMvcConfigurer, SecurityFilterChain as a bean: these are the extension points the framework was designed for. Replacing auto-configured beans directly works sometimes, but the ordering behavior is fragile.

Exclude explicitly instead of fighting conditionals. If auto-configuration is activating something you don't want, exclude it. It's clean, it's documented, and it's obvious to the next person who reads the code.

The four common conditions are worth knowing cold: @ConditionalOnClass, @ConditionalOnMissingBean, @ConditionalOnProperty, @ConditionalOnSingleCandidate. These cover 80% of what auto-configuration uses. Read the annotation on the auto-configuration class before trying to override it and you'll immediately understand what you're working with.

It stops feeling like magic once you read the source for one auto-configuration class. Pick DataSourceAutoConfiguration. It's 50 lines. After that, the rest follow the same pattern, and the magic becomes a very well-organized set of conditionals that you can read, override, and exclude whenever you need to.

Share
X LinkedIn HN
UI

Umur Inan

Principal Software Engineer

Backend engineer focused on JVM systems, distributed architecture, and the failure modes that only show up in production. I write about what I learn building and breaking things at scale.

👁 0 5 min read

Comments (0)