How Spring Boot actually decides what to configure automatically: the @Conditional family, writing a custom starter, and the five pillars — auto-config, starters, embedded server, externalized config, and Actuator.
Published September 23, 2026
What is Spring Boot? introduced auto-configuration at a high level — "add a starter, get beans for free." This lesson covers the actual mechanism making that happen.
Spring Boot 3+ discovers auto-configuration classes via a file at META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports — a plain list of fully-qualified class names to consider. Before Spring Boot 3.0, the same role was played by an entry under org.springframework.boot.autoconfigure.EnableAutoConfiguration in META-INF/spring.factories, a more general-purpose (and more cluttered) mechanism shared with other Spring SPI hooks. The 3.0 change split auto-configuration discovery into its own dedicated file, purely for clarity and faster startup scanning — functionally, both mechanisms answer the same question: "which auto-configuration classes exist to consider."
Being discovered doesn't mean a configuration class's beans get created — each auto-configuration class is typically covered in @Conditional* annotations that gate whether it actually activates:
@Configuration
@ConditionalOnClass(MongoClient.class) // only activate if the Mongo driver is on the classpath
@ConditionalOnMissingBean(MongoTemplate.class) // back off if the developer already defined their own
class MongoAutoConfiguration {
@Bean
@ConditionalOnProperty(name = "app.mongo.enabled", havingValue = "true", matchIfMissing = true)
MongoTemplate mongoTemplate(MongoClient client) { return new MongoTemplate(client); }
}
@ConditionalOnClass — only activate if a specific dependency is present on the classpath. This is why adding spring-boot-starter-data-mongodb "magically" configures a MongoTemplate: the condition checks for MongoClient.class, which that starter transitively pulls in.@ConditionalOnMissingBean — back off entirely if the developer already defined their own bean of that type. This is the specific mechanism that makes auto-configuration overridable rather than a fixed, unchangeable default — define your own MongoTemplate bean, and Boot's auto-configured one simply never activates.@ConditionalOnProperty — feature-flag-style activation, gated on an application.yml property value.@ConditionalOnWebApplication / @ConditionalOnNotWebApplication — activate only in a web (servlet-based) context, or only in a non-web context, respectively — this is how the same Spring Boot dependency graph can auto-configure an embedded Tomcat only when it's actually relevant.A starter is really two artifacts: an autoconfigure module (the @Configuration class with its @Conditional beans, exactly the shape above) plus @ConfigurationProperties for type-safe, externally-configurable settings:
@ConfigurationProperties(prefix = "myservice")
class MyServiceProperties {
private String apiKey;
private int timeoutSeconds = 30; // sensible default
// getters/setters
}
@Configuration
@EnableConfigurationProperties(MyServiceProperties.class)
@ConditionalOnClass(MyServiceClient.class)
class MyServiceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
MyServiceClient myServiceClient(MyServiceProperties props) {
return new MyServiceClient(props.getApiKey(), props.getTimeoutSeconds());
}
}
A consuming application just adds the starter as a dependency and sets myservice.api-key in application.yml — no @Bean method of their own required, unless they want to override it.
@Component
class DataSeeder implements CommandLineRunner {
public void run(String... args) { seedInitialData(); } // runs once, after the context is FULLY loaded
}
Beans implementing either interface have their run() method executed exactly once, automatically, right after the application context finishes loading — the standard place for startup data seeding or startup-time validation, without needing to hook into the bean lifecycle manually.
A normal JAR contains only your own compiled classes — running it requires separately placing every dependency JAR on the classpath. A fat (executable) JAR bundles all dependencies plus an embedded server inside one self-contained archive, which is exactly what spring-boot-maven-plugin/spring-boot-gradle-plugin produce by default — java -jar myapp.jar just works, with zero external classpath setup, because everything needed is already inside that one file.
Spring Boot auto-configures Logback as the default logging implementation via spring-boot-starter-logging, with sensible console output requiring zero setup. Server port resolves via a defined precedence: command-line argument (--server.port=9090) > environment variable (SERVER_PORT) > application.yml/properties > default 8080 — this ordering follows Spring's general externalized-configuration precedence (more specific/runtime sources override more general/build-time ones), and it's exactly what Convention over Configuration means as Spring Boot's core design philosophy: sensible defaults apply automatically, and you only specify what genuinely differs from the convention — you never have to declare the default port, the default logging framework, or the default server, only override them when needed.
Spring Boot inspects the classpath for a web starter dependency — finding spring-boot-starter-web (which transitively includes spring-boot-starter-tomcat), it auto-configures an embedded servlet container without any explicit @Bean for a server. This is the same @ConditionalOnClass mechanism as the Mongo example above, just applied to the embedded-server decision specifically.
@RestController is exactly @Controller + @ResponseBody applied automatically to every method — meaning return values are serialized directly into the HTTP response body (typically as JSON, via Jackson), rather than being resolved as a view name for server-side template rendering. Nearly every Spring Boot API (as opposed to a server-rendered web app) uses @RestController for this reason.
@ControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
return ResponseEntity.status(404).body(new ErrorResponse(ex.getMessage()));
}
}
Centralizing exception-to-HTTP-response translation in one @ControllerAdvice class means individual controllers stay free of repetitive try/catch blocks — every controller in the application benefits from the same consistent error-response shape, defined once.
What distinguishes Spring Boot from plain Spring, stated as a checklist: auto-configuration (this lesson), embedded server (no WAR deployment), starter dependencies (curated bundles), externalized configuration (the precedence chain above), and Actuator (production-ready monitoring endpoints, out of scope for this lesson but worth naming as the fifth pillar).
Q: What happens if two different starters' auto-configuration classes both try to configure a bean of the same type?
A: @ConditionalOnMissingBean is exactly the mechanism preventing conflict here too — Spring Boot's auto-configuration classes are ordered (via @AutoConfigureOrder/@AutoConfigureBefore/@AutoConfigureAfter), and whichever runs first successfully creates the bean; later auto-configurations checking @ConditionalOnMissingBean for the same type simply back off, avoiding a duplicate-bean conflict.
Q: Why does @ConditionalOnClass check the classpath instead of checking application.yml directly?
A: Classpath presence and explicit configuration answer different questions — @ConditionalOnClass answers "could this feature even work, given what's installed," while @ConditionalOnProperty answers "has the developer explicitly asked for this behavior." Using classpath presence lets a dependency's mere inclusion trigger sensible auto-configuration without also requiring redundant explicit opt-in configuration for every single feature.
Q: Is @ConfigurationProperties an alternative to @Value, or do they serve different needs?
A: @ConfigurationProperties binds an entire group of related properties into one typed object in one place (as shown above), while @Value injects a single property value at a single injection point — @ConfigurationProperties scales much better for a starter or module with many related settings, since it centralizes them into one validated, typed class rather than scattering individual @Value annotations across many classes.
Q: Could a poorly-written custom starter break auto-configuration for an unrelated part of the application?
A: Yes — if a custom starter's @Conditional checks are too broad or its bean definitions collide with another module's beans without proper @ConditionalOnMissingBean guards, it can either fail to back off when it should or inadvertently prevent a legitimate bean elsewhere from being created, which is why careful, narrow conditions are considered a best practice when writing a starter meant for reuse across multiple applications.