Spring Boot Auto-Configuration: How It Works and When to Override It
You’ve added a single dependency to your pom.xml, restarted your application, and suddenly it connects to a database, configures HikariCP with sensible defaults, and wraps your repository calls in transactions. No XML. No explicit bean definitions. Just one line in your dependency list and everything works.
Until it doesn’t.
Maybe you’ve spent an afternoon wondering why your custom DataSource configuration is being ignored. Or you’ve watched your application fail to start because auto-configuration pulled in a component you didn’t want. Perhaps you’ve seen two auto-configured beans fight for precedence, leaving you with neither. The “magic” of Spring Boot auto-configuration is fantastic when it aligns with your intentions—and genuinely frustrating when it doesn’t, because the conventional debugging approach of reading configuration files doesn’t apply when there are no configuration files to read.
The root cause of this frustration is a knowledge gap. Spring Boot’s auto-configuration isn’t actually magic; it’s a well-defined mechanism with predictable rules. Every auto-configured bean follows a specific lifecycle, evaluates explicit conditions before activating, and respects a clear hierarchy of precedence. When you understand this mechanism, you stop fighting the framework and start leveraging it. You know exactly where to look when something misfires. You write custom configurations that integrate cleanly with auto-configuration rather than competing against it.
This understanding starts with how Spring Boot discovers and evaluates auto-configuration classes in the first place—a process that begins the moment your application context starts loading.
The Auto-Configuration Lifecycle: From JAR to Running Application
When you annotate your main class with @SpringBootApplication, you’re triggering a sophisticated discovery and configuration pipeline that transforms a collection of JARs into a fully-wired application. Understanding this lifecycle is essential for debugging configuration issues and predicting how Spring Boot will behave with your custom beans.

Discovery: Finding Auto-Configuration Classes
The journey begins when Spring Boot scans the classpath for auto-configuration candidates. Each Spring Boot starter and auto-configuration module includes a file at META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. This file contains a simple list of fully-qualified class names—one per line—representing potential configuration classes.
This mechanism replaced the older spring.factories approach in Spring Boot 3.0, offering better performance through lazy loading and clearer separation of concerns. When your application starts, Spring Boot collects all these files from every JAR on the classpath and builds a master list of configuration candidates.
💡 Pro Tip: You can examine which auto-configuration classes are available in any starter by extracting its JAR and checking the
AutoConfiguration.importsfile. This is faster than searching documentation when debugging unexpected behavior.
Evaluation: The Conditional Gate
Having a class listed in AutoConfiguration.imports doesn’t guarantee it will be applied. Each auto-configuration class is decorated with one or more @Conditional annotations that act as gates. Spring Boot evaluates these conditions to determine whether the configuration should activate.
Common conditions include checking for the presence of specific classes on the classpath, the existence of particular beans in the context, or the values of configuration properties. Only when all conditions on a configuration class evaluate to true does Spring Boot proceed to process that class and register its beans.
This conditional evaluation happens during the bean definition phase, before any beans are instantiated. Spring Boot reads the annotations, evaluates the conditions, and either includes or excludes each configuration class from further processing.
Ordering: Sequence Matters
Auto-configuration classes don’t execute in random order. Spring Boot uses the @AutoConfigureOrder, @AutoConfigureBefore, and @AutoConfigureAfter annotations to establish a deterministic sequence. This ordering is critical because later configurations can see beans registered by earlier ones, and @ConditionalOnMissingBean checks depend on what’s already been defined.
Your own beans, defined in @Configuration classes within your application package, are processed before auto-configuration runs. This deliberate sequencing is why defining your own DataSource bean prevents Spring Boot from creating one—your bean exists when the auto-configuration’s @ConditionalOnMissingBean check executes.
Understanding this ordering explains a common debugging scenario: when two auto-configurations seem to conflict, the resolution often lies in their relative ordering and which conditions they check.
Now that you understand how Spring Boot discovers and applies auto-configuration, let’s examine how to inspect exactly what configuration decisions it made for your running application.
Inspecting What Spring Boot Actually Configured
When auto-configuration behaves unexpectedly, you need visibility into Spring Boot’s decision-making process. Understanding what got configured and why transforms debugging from guesswork into systematic analysis. This section covers three complementary approaches: startup-time diagnostics, runtime inspection, and source code analysis.
The Debug Flag and Condition Evaluation Report
The fastest way to inspect auto-configuration decisions is launching your application with the --debug flag:
java -jar myapp.jar --debugAlternatively, set debug=true in your application.properties. This activates the ConditionEvaluationReport, which logs every auto-configuration class and the conditions that determined its fate.
The output splits into two sections: Positive matches show what was configured, and Negative matches reveal what was skipped:
============================CONDITIONS EVALUATION REPORT============================
Positive matches:----------------- DataSourceAutoConfiguration matched: - @ConditionalOnClass found required classes 'javax.sql.DataSource', 'org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType'
DataSourceAutoConfiguration.PooledDataSourceConfiguration matched: - AnyNestedCondition 'DataSourceAutoConfiguration.PooledDataSourceCondition' matched - @ConditionalOnMissingBean (types: javax.sql.DataSource) did not find any beans
Negative matches:----------------- MongoAutoConfiguration: Did not match: - @ConditionalOnClass did not find required class 'com.mongodb.client.MongoClient'This report answers the critical question: “Why did Spring Boot create (or not create) this bean?” Each entry shows the exact condition that passed or failed. When troubleshooting missing functionality, start with negative matches—they often reveal a missing dependency or an unexpected bean already present in the context.
The report also includes an Unconditional classes section listing auto-configurations that have no conditions at all. These always activate when present on the classpath, which can explain unexpected behavior when transitive dependencies pull in unwanted starters.
Runtime Inspection with Actuator
For running applications, Spring Boot Actuator’s /conditions endpoint provides the same information via HTTP. This proves invaluable when debugging production issues where restarting with --debug isn’t feasible:
implementation 'org.springframework.boot:spring-boot-starter-actuator'management.endpoints.web.exposure.include=conditions,beans,envQuery the endpoint to get JSON-formatted condition evaluation data:
curl http://localhost:8080/actuator/conditions | jq '.contexts.application.positiveMatches'The /beans endpoint complements this by listing all registered beans with their dependencies and scope. The /env endpoint reveals the resolved property values that influence conditional evaluation. Together, these endpoints provide complete runtime visibility without restarting your application.
For security-sensitive environments, consider exposing these endpoints only on a separate management port or requiring authentication. The information they reveal about your application’s internals could aid attackers if exposed publicly.
💡 Pro Tip: Pipe actuator output through
jqwith filters like.contexts.application.positiveMatches | keys | map(select(contains("DataSource")))to quickly find configuration classes related to specific features.
Reading Auto-Configuration Source Code
When the condition report points to an auto-configuration class, reading its source reveals the complete picture. Spring Boot’s auto-configuration classes follow a consistent structure that becomes predictable once you recognize the patterns:
@AutoConfiguration(before = SqlInitializationAutoConfiguration.class)@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")@EnableConfigurationProperties(DataSourceProperties.class)public class DataSourceAutoConfiguration {
@Configuration(proxyBeanMethods = false) @Conditional(PooledDataSourceCondition.class) @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) static class PooledDataSourceConfiguration { // HikariCP, Tomcat, DBCP2 configuration logic }}Key elements to examine: the @Conditional annotations define activation rules, @EnableConfigurationProperties links to externalized configuration, and nested @Configuration classes handle specific scenarios. The before and after attributes in @AutoConfiguration establish ordering relationships that can explain why one configuration sees or doesn’t see beans from another.
Navigate directly to auto-configuration sources in your IDE by searching for classes ending in AutoConfiguration. The spring-boot-autoconfigure module contains all standard configurations, organized by technology (data, web, security). Most IDEs support navigating to library sources—in IntelliJ, use Ctrl+Click on any auto-configuration class reference.
Understanding the condition annotations that control these classes—@ConditionalOnClass, @ConditionalOnBean, @ConditionalOnProperty, @ConditionalOnMissingBean—gives you the vocabulary to predict and influence auto-configuration behavior. The @ConditionalOnMissingBean pattern is particularly important: it’s how Spring Boot backs off when you provide your own implementation, and understanding this pattern helps you write configurations that integrate cleanly with the framework’s defaults.
The @Conditional Family: Understanding Configuration Triggers
Spring Boot’s auto-configuration appears magical until you understand the conditional system driving it. Every auto-configuration class uses one or more @Conditional annotations to determine whether it should activate. Mastering these conditions gives you precise control over which beans Spring creates—and which ones your custom configurations override.
The Core Conditional Annotations
Three conditional annotations form the backbone of Spring Boot’s auto-configuration decisions:
@ConditionalOnClass activates a configuration only when specific classes exist on the classpath. This is how Spring Boot detects which technologies you’re using:
@Configuration@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })public class DataSourceAutoConfiguration { // Only activates when JDBC classes are present}When you add spring-boot-starter-data-jpa to your dependencies, the JPA classes appear on the classpath, triggering the corresponding auto-configuration. Remove the starter, and the configuration silently deactivates—no code changes required.
@ConditionalOnMissingBean is the annotation that makes your custom beans take precedence. Auto-configuration classes use this to step aside when you’ve defined your own implementation:
@Bean@ConditionalOnMissingBeanpublic ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) { return builder.createXmlMapper(false).build();}Define your own ObjectMapper bean anywhere in your application, and Spring Boot’s version never gets created. This annotation can also target specific bean types or names, allowing fine-grained control over which beans trigger the condition.
@ConditionalOnProperty enables configuration based on property values, giving you toggle switches for features:
@Configuration@ConditionalOnProperty(prefix = "spring.cache", name = "enabled", havingValue = "true", matchIfMissing = true)public class CacheAutoConfiguration { // Activated unless spring.cache.enabled=false}The matchIfMissing parameter determines behavior when the property isn’t set—a subtle detail that catches many developers off guard. Setting it to true means the feature enables by default; false requires explicit opt-in.
Condition Composition and Evaluation Order
Conditions compose through logical AND semantics. When multiple conditions appear on a single class or method, all must pass for activation:
@Configuration@ConditionalOnClass(RedisOperations.class)@ConditionalOnProperty(prefix = "spring.redis", name = "enabled", matchIfMissing = true)@ConditionalOnMissingBean(RedisConnectionFactory.class)public class RedisAutoConfiguration { // Requires: Redis classes present AND property not disabled AND no custom factory}Spring evaluates conditions in a specific order optimized for performance. Class-presence conditions (@ConditionalOnClass) run first since they’re cheapest to evaluate—a simple classloader check. Property conditions run next, requiring only environment access. Bean-presence conditions (@ConditionalOnMissingBean) run last because they require the bean registry to be partially populated, making them the most expensive to evaluate.
This ordering matters when you’re debugging configuration issues. A failing @ConditionalOnClass check prevents Spring from even evaluating the other conditions, so always verify classpath issues before investigating property or bean-related problems.
💡 Pro Tip: When debugging why a configuration isn’t activating, check conditions in order: classpath first, then properties, then bean presence. The actuator’s
/conditionsendpoint shows exactly which condition failed and why.
Creating Custom Conditions
For complex scenarios, implement the Condition interface directly:
public class OnProductionEnvironmentCondition implements Condition {
@Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { Environment env = context.getEnvironment(); String region = env.getProperty("app.deployment.region", "us-east-1"); String tier = env.getProperty("app.deployment.tier", "development");
return "production".equals(tier) && !region.startsWith("local"); }}The ConditionContext provides access to the bean registry, environment, resource loader, and class loader—everything you need to make sophisticated activation decisions. The AnnotatedTypeMetadata parameter lets you read annotation attributes, enabling parameterized conditions.
Apply it with the generic @Conditional annotation:
@Configuration@Conditional(OnProductionEnvironmentCondition.class)public class ProductionSecurityConfig {
@Bean public SecurityAuditLogger securityAuditLogger() { return new CloudWatchSecurityAuditLogger("prod-security-logs"); }}For reusable conditions, create a composed annotation:
@Target({ ElementType.TYPE, ElementType.METHOD })@Retention(RetentionPolicy.RUNTIME)@Conditional(OnProductionEnvironmentCondition.class)public @interface ConditionalOnProduction {}This pattern appears throughout Spring Boot’s internals and keeps configuration classes clean and declarative. You can stack multiple meta-annotations to create sophisticated activation rules that remain readable at the point of use.
Understanding conditions transforms auto-configuration from opaque magic into a predictable system. When defaults don’t fit your requirements, you have three distinct strategies for taking control—each appropriate for different scenarios.
Overriding Auto-Configuration: Three Strategies
Spring Boot’s auto-configuration provides sensible defaults, but production systems frequently demand customization. Understanding when and how to override these defaults separates developers who fight the framework from those who work with it. Each override strategy offers a different level of control and carries distinct maintenance implications.

Strategy 1: Define Your Own Bean
The most elegant override leverages @ConditionalOnMissingBean—the condition that powers most auto-configuration classes. When you define a bean of the same type, Spring Boot’s auto-configuration detects it and backs off.
@Configurationpublic class CustomDataSourceConfig {
@Bean public DataSource dataSource() { HikariDataSource dataSource = new HikariDataSource(); dataSource.setJdbcUrl("jdbc:postgresql://db.example.com:5432/orders"); dataSource.setUsername("app_user"); dataSource.setPassword("secure_password_here"); dataSource.setMaximumPoolSize(25); dataSource.setConnectionTimeout(30000); dataSource.setIdleTimeout(600000); return dataSource; }}When Spring Boot’s DataSourceAutoConfiguration runs, it checks for an existing DataSource bean. Finding yours, it skips its own configuration entirely. This approach preserves all other auto-configuration behavior while giving you complete control over one specific bean.
💡 Pro Tip: Name your bean method to match the auto-configured bean name when possible. This prevents subtle issues where code expects a bean with a specific name. Check the auto-configuration source to find the expected name.
This strategy works best when you need full programmatic control over bean construction—reading secrets from a vault, applying complex initialization logic, or wiring dependencies that properties cannot express.
Strategy 2: Externalize with Properties
For simpler customizations, application.properties or application.yml offers the path of least resistance. Auto-configured beans bind to configuration properties, exposing common customization points without requiring Java code.
## DataSource configurationspring.datasource.url=jdbc:postgresql://db.example.com:5432/ordersspring.datasource.username=app_userspring.datasource.password=${DB_PASSWORD}spring.datasource.hikari.maximum-pool-size=25spring.datasource.hikari.connection-timeout=30000spring.datasource.hikari.idle-timeout=600000
## Server configurationserver.port=8443server.ssl.key-store=classpath:keystore.p12server.ssl.key-store-password=${KEYSTORE_PASSWORD}server.ssl.key-store-type=PKCS12Properties-based configuration integrates naturally with Spring’s environment abstraction. You gain profile-specific overrides, environment variable substitution, and externalized configuration without touching Java code. The auto-configuration class still creates the bean—you just influence its parameters.
This strategy fits when the auto-configured bean structure matches your needs but requires different values. It fails when you need a fundamentally different implementation or complex initialization that properties cannot capture.
Strategy 3: Exclude the Auto-Configuration Entirely
When an entire auto-configuration class conflicts with your architecture, exclude it completely. This nuclear option removes the auto-configuration from consideration during startup.
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class, SecurityAutoConfiguration.class})public class OrderServiceApplication {
public static void main(String[] args) { SpringApplication.run(OrderServiceApplication.class, args); }}Alternatively, configure exclusions externally:
spring.autoconfigure.exclude=\ org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\ org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfigurationExclusion makes sense when you use a competing library that provides its own configuration, when auto-configuration detects dependencies you don’t want activated, or when you need to prevent specific functionality from loading in certain environments.
💡 Pro Tip: After excluding an auto-configuration, run your application with
--debugto verify the exclusion took effect. The auto-configuration report shows excluded classes in a dedicated section.
Choosing Your Strategy
The decision framework is straightforward: use properties when tweaking values, define beans when needing programmatic control, and exclude when rejecting entire configuration modules. Properties require the least code and adapt easily across environments. Custom beans offer precision without abandoning other auto-configuration benefits. Exclusion provides a clean break when you need complete independence.
Most production applications combine all three strategies across different subsystems. Understanding the trade-offs lets you pick the right tool for each situation.
With these override strategies in hand, examining a complete real-world scenario demonstrates how they work together. The next section walks through customizing DataSource auto-configuration from initial requirements through production deployment.
Case Study: Customizing DataSource Auto-Configuration
Database configuration is where Spring Boot’s auto-configuration shines brightest—and where it causes the most confusion when defaults don’t fit enterprise requirements. Let’s dissect how DataSourceAutoConfiguration works and master the patterns for taking control.
How DataSourceAutoConfiguration Works Under the Hood
When Spring Boot detects a JDBC driver on the classpath, DataSourceAutoConfiguration activates. The class uses a hierarchy of @Conditional annotations to make intelligent decisions:
- It checks for an existing
DataSourcebean (@ConditionalOnMissingBean) - It detects available connection pool libraries (HikariCP, Tomcat, DBCP2)
- It binds
spring.datasource.*properties to the chosen pool implementation
HikariCP takes priority since Spring Boot 2.0. The auto-configuration creates a single DataSource bean, configures it from your properties, and wires it into JPA, JDBC templates, and transaction managers automatically.
Understanding this sequence matters because it reveals your intervention points. If you define your own DataSource bean, the auto-configuration backs off entirely due to @ConditionalOnMissingBean. If you need partial customization, you can define a DataSourceProperties bean instead, letting auto-configuration handle the pool creation while you control the connection parameters.
Configuring Multiple Data Sources
Enterprise applications frequently require connections to multiple databases—a primary transactional store, a read replica for reporting, or a separate database for audit logs. This is where you must step outside auto-configuration’s comfort zone.
First, disable the default auto-configuration and define your data sources explicitly:
@Configurationpublic class DataSourceConfig {
@Bean @Primary @ConfigurationProperties("app.datasource.primary") public DataSource primaryDataSource() { return DataSourceBuilder.create().build(); }
@Bean @ConfigurationProperties("app.datasource.reporting") public DataSource reportingDataSource() { return DataSourceBuilder.create().build(); }}The @ConfigurationProperties annotation binds all properties under the specified prefix to the DataSource. Your application.yml becomes:
app: datasource: primary: jdbc-url: jdbc:postgresql://primary-db.internal:5432/orders username: app_user password: ${PRIMARY_DB_PASSWORD} driver-class-name: org.postgresql.Driver reporting: jdbc-url: jdbc:postgresql://replica-db.internal:5432/orders username: readonly_user password: ${REPORTING_DB_PASSWORD} driver-class-name: org.postgresql.Driver💡 Pro Tip: Use
jdbc-urlinstead ofurlwhen configuring HikariCP directly. The property name differs from Spring’s genericspring.datasource.urlbinding. This subtle distinction causes countless configuration failures in multi-datasource setups.
The @Primary annotation ensures that components expecting a single DataSource (like Spring Data repositories) receive the primary connection without additional configuration. Components needing the secondary source must use @Qualifier("reportingDataSource") to specify which bean they require.
HikariCP Tuning for Production
Default pool settings work for development but require tuning for production workloads. Understanding these properties prevents connection exhaustion and performance degradation:
app: datasource: primary: jdbc-url: jdbc:postgresql://primary-db.internal:5432/orders username: app_user password: ${PRIMARY_DB_PASSWORD} hikari: maximum-pool-size: 20 minimum-idle: 5 idle-timeout: 300000 connection-timeout: 20000 max-lifetime: 1200000 leak-detection-threshold: 60000Key tuning considerations:
- maximum-pool-size: Calculate based on your database’s connection limits divided by application instances. A PostgreSQL default of 100 connections across 5 instances means 20 per instance maximum. Oversizing this value leads to database connection exhaustion during deployments when old and new instances briefly coexist.
- minimum-idle: Keep this lower than maximum to allow the pool to shrink during low-traffic periods. Setting these values equal creates a fixed-size pool, which simplifies capacity planning but wastes resources during quiet hours.
- max-lifetime: Set this below your database’s connection timeout (typically 30 minutes for PostgreSQL) to prevent stale connection errors. HikariCP will gracefully retire connections before the database forcibly closes them.
- leak-detection-threshold: Enable this in staging environments to catch connections not properly returned to the pool. Set it slightly above your slowest expected query time. In production, disable it to avoid the performance overhead of tracking connection acquisition times.
For applications using JPA with multiple data sources, you’ll also need separate EntityManagerFactory and TransactionManager beans for each data source—a pattern that builds directly on the configuration shown here.
💡 Pro Tip: Monitor your connection pool metrics in production using Spring Boot Actuator’s
/actuator/metrics/hikaricp.*endpoints. Watch forhikaricp.connections.pendingspikes, which indicate pool exhaustion. Thehikaricp.connections.usagemetric reveals how efficiently your application utilizes available connections.
This pattern of understanding auto-configuration internals, then surgically replacing specific beans, applies beyond data sources. In the next section, we’ll explore how to package these customizations into your own reusable auto-configuration module.
Writing Your Own Auto-Configuration Module
When your organization operates multiple microservices, you’ll inevitably encounter shared configuration patterns: custom security policies, standardized logging formats, common health indicators, or proprietary client libraries. Rather than copying configuration classes across repositories, you can package this logic into a reusable Spring Boot starter that integrates seamlessly with the auto-configuration mechanism. This approach reduces duplication, enforces consistency, and gives consuming applications the same frictionless experience they expect from official Spring Boot starters.
Anatomy of a Custom Starter
A well-structured starter consists of two modules: an autoconfigure module containing the configuration logic, and a starter module that pulls in dependencies. The autoconfigure module houses your @AutoConfiguration classes, conditional annotations, and properties classes. The starter module acts as an aggregator, declaring dependencies on the autoconfigure module plus any required runtime libraries. For simpler cases where you have minimal dependencies and straightforward configuration, you can combine these into a single module without sacrificing maintainability.
@AutoConfiguration@ConditionalOnClass(MeterRegistry.class)@EnableConfigurationProperties(CompanyMetricsProperties.class)public class CompanyMetricsAutoConfiguration {
@Bean @ConditionalOnMissingBean public CompanyMetricsExporter metricsExporter( MeterRegistry registry, CompanyMetricsProperties properties) { return new CompanyMetricsExporter(registry, properties.getEndpoint()); }
@Bean @ConditionalOnProperty( prefix = "company.metrics", name = "business-metrics-enabled", havingValue = "true", matchIfMissing = true) public BusinessMetricsRecorder businessMetricsRecorder(MeterRegistry registry) { return new BusinessMetricsRecorder(registry); }}The @ConditionalOnMissingBean annotation is particularly important here—it allows consuming applications to provide their own implementation, and your auto-configured bean will back off gracefully. This pattern respects the principle that application-defined beans should always take precedence over auto-configured defaults.
Register your auto-configuration class in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:
com.company.metrics.autoconfigure.CompanyMetricsAutoConfigurationControlling Configuration Order
When your auto-configuration depends on beans created by other auto-configurations, ordering becomes critical. Spring Boot provides explicit ordering annotations that give you precise control over when your configuration runs relative to others. Incorrect ordering is one of the most common sources of subtle bugs in custom starters—your configuration might silently fail to find expected beans or inadvertently override beans that should take precedence.
@AutoConfiguration( after = SecurityAutoConfiguration.class, before = WebMvcAutoConfiguration.class)@ConditionalOnWebApplication(type = Type.SERVLET)public class CompanySecurityAutoConfiguration {
@Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SecurityFilterChain companySecurityFilterChain(HttpSecurity http) throws Exception { return http .authorizeHttpRequests(auth -> auth .requestMatchers("/internal/**").hasRole("INTERNAL_SERVICE") .anyRequest().authenticated()) .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())) .build(); }}💡 Pro Tip: Use
afterwhen your configuration consumes beans from another auto-configuration. Usebeforewhen you provide beans that another auto-configuration should consume or when you need to set up infrastructure before another configuration runs.
Testing Auto-Configuration
Thorough testing is essential for auto-configuration modules since they must handle a wide variety of classpath conditions and property combinations. Spring Boot provides two complementary approaches: lightweight testing with ApplicationContextRunner for fast, focused unit tests, and full integration testing with @SpringBootTest for end-to-end validation.
The ApplicationContextRunner provides a lightweight way to test auto-configuration without spinning up a full application context:
class CompanyMetricsAutoConfigurationTest {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration( AutoConfigurations.of(CompanyMetricsAutoConfiguration.class));
@Test void shouldCreateMetricsExporterWhenMeterRegistryPresent() { contextRunner .withBean(MeterRegistry.class, SimpleMeterRegistry::new) .withPropertyValues("company.metrics.endpoint=https://metrics.company.com") .run(context -> { assertThat(context).hasSingleBean(CompanyMetricsExporter.class); assertThat(context).hasSingleBean(BusinessMetricsRecorder.class); }); }
@Test void shouldBackOffWhenCustomExporterProvided() { contextRunner .withBean(MeterRegistry.class, SimpleMeterRegistry::new) .withBean(CompanyMetricsExporter.class, () -> mock(CompanyMetricsExporter.class)) .run(context -> { assertThat(context).hasSingleBean(CompanyMetricsExporter.class); // Verify the mock is used, not our auto-configured bean }); }
@Test void shouldNotLoadWhenMeterRegistryAbsent() { contextRunner .run(context -> { assertThat(context).doesNotHaveBean(CompanyMetricsExporter.class); }); }}This approach lets you verify conditional logic, property binding, and bean creation without the overhead of starting a Spring application. You can simulate different classpath conditions, property configurations, and existing bean scenarios in milliseconds rather than seconds. For more complex scenarios involving actual HTTP requests or database connections, supplement these tests with @SpringBootTest integration tests that validate the complete auto-configuration behavior in a realistic environment.
When your auto-configuration works correctly, consuming applications simply add your starter dependency and optionally override properties—the same experience developers expect from official Spring Boot starters. Document your configuration properties thoroughly and consider providing a spring-configuration-metadata.json file to enable IDE autocompletion for your custom properties.
With custom auto-configuration modules in place, you’ll want to understand how these patterns behave under production load and what pitfalls to avoid when debugging configuration issues at scale.
Production Considerations and Common Pitfalls
Understanding auto-configuration theory is one thing; keeping it from causing 3 AM pages is another. This section covers the sharp edges that catch even experienced teams.
Bean Definition Order in Distributed Systems
Auto-configuration assumes a single-JVM mental model. In distributed deployments, this assumption breaks down in subtle ways. When multiple services share configuration classes through a common library, the order in which beans initialize can vary across nodes due to classpath scanning differences, JVM warm-up characteristics, or even filesystem ordering on different OS versions.
The practical impact: a service that works perfectly in staging fails intermittently in production because one node initialized its cache client before the connection pool, while another did the opposite. The fix is explicit dependency declarations using @DependsOn or, better, restructuring to eliminate implicit ordering requirements entirely.
Classpath Pollution and Unwanted Auto-Configuration
Every JAR on your classpath is a potential auto-configuration trigger. That transitive dependency on spring-boot-starter-data-redis pulled in by a utility library? It just enabled Redis auto-configuration across your entire application, even if you never intended to use Redis.
Run mvn dependency:tree or gradle dependencies regularly and audit what’s actually on your classpath. Use explicit exclusions in your starter dependencies, and consider the spring-boot-autoconfigure-exclude property to disable problematic auto-configurations without restructuring your dependencies.
💡 Pro Tip: Add the
spring-boot-starter-actuatordependency and check/actuator/conditionsin staging before every production deployment. New auto-configurations appearing unexpectedly indicate dependency changes that warrant investigation.
Conditional Evaluation Performance
Every @Conditional annotation runs evaluation logic at startup. In applications with hundreds of auto-configuration classes, this adds measurable seconds to boot time. The impact compounds in environments with frequent restarts or scale-out events.
Profile your startup using Spring Boot’s startup actuator endpoint. If conditional evaluation dominates, consider replacing auto-configuration with explicit bean definitions for your core infrastructure. You trade some convenience for predictable, faster startups.
These production concerns make a strong case for the next level of control: creating your own auto-configuration modules that encode your organization’s specific requirements and constraints.
Key Takeaways
- Run your application with —debug to generate a ConditionEvaluationReport whenever configuration behaves unexpectedly
- Define your own @Bean method to override any auto-configured bean—Spring Boot’s @ConditionalOnMissingBean will back off automatically
- Create a custom spring-boot-starter for configuration shared across your microservices to maintain consistency and reduce boilerplate
- Use @AutoConfigureOrder and @AutoConfigureAfter when your configuration depends on other auto-configurations being present