What a Starter Actually Is — Two Things, Not One
The name "starter" suggests a single mechanism. It's actually two separate things working together, and conflating them is the source of most "why did adding this dependency change my app's behaviour" confusion:
| Part | What it is | Where the code lives |
|---|---|---|
| The starter POM | An empty JAR whose only content is a
pom.xml declaring a curated set of
transitive dependencies at compatible versions |
No Java code at all — pure Maven dependency management |
| Auto-configuration | @AutoConfiguration classes in the
pulled-in libraries that Spring Boot registers and
conditionally activates |
Inside the actual library JARs
(spring-webmvc,
jackson-databind, etc.), not in the
starter itself |
Adding spring-boot-starter-web to your POM does two things at
once: it pulls in spring-webmvc, spring-boot-starter-tomcat,
jackson-databind, and a handful of others (the starter's job),
and those JARs each ship @AutoConfiguration classes that Spring
Boot discovers and conditionally activates (auto-configuration's job). The
starter is the trigger; the auto-configuration is the actual behaviour
change.
Since Spring Boot 2.7, each library that provides auto-configuration
ships a file at
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
listing its @AutoConfiguration class names. Spring Boot
reads this file on startup and registers every class in it as a
candidate. Before 2.7, the same information lived in
META-INF/spring.factories under the
EnableAutoConfiguration key — that file still works for
backwards compatibility, but new libraries use
AutoConfiguration.imports.
Common Starters and What They Actually Pull In
| Starter | Key transitive dependencies | Auto-configuration effect |
|---|---|---|
spring-boot-starter-web |
spring-webmvc, embedded Tomcat, jackson-databind | Configures DispatcherServlet, starts Tomcat, registers Jackson HttpMessageConverter |
spring-boot-starter-data-jpa |
spring-data-jpa, Hibernate, spring-jdbc, HikariCP | Configures EntityManagerFactory, connection pool, transaction manager, repository scanning |
spring-boot-starter-security |
spring-security-web, spring-security-config | Registers DelegatingFilterProxy, secures all endpoints by default — see Spring Security |
spring-boot-starter-validation |
jakarta.validation-api, hibernate-validator | Registers MethodValidationPostProcessor; enables @Valid on controller parameters |
spring-boot-starter-test |
JUnit 5, Mockito, AssertJ, Spring Test, Hamcrest | No runtime auto-configuration — test-scope only |
spring-boot-starter-actuator |
spring-boot-actuator, Micrometer | Exposes /actuator/health, /actuator/metrics and other management endpoints |
spring-boot-starter-cache |
spring-context-support | Enables @EnableCaching infrastructure; a cache provider (Caffeine, Redis) must still be added separately |
spring-boot-starter-webflux |
spring-webflux, Reactor Netty | Configures reactive DispatcherHandler on Netty — mutually exclusive with starter-web on the same classpath |
<!-- pom.xml — no <version> needed: spring-boot-starter-parent manages all versions -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Excluding Auto-Configuration
Auto-configuration is conditional — every @AutoConfiguration
class is gated by one or more
@ConditionalOn* annotations (@ConditionalOnClass,
@ConditionalOnMissingBean, etc.) and only activates when those
conditions are met. Usually this means it stays out of your way — the moment
you define your own DataSource bean,
DataSourceAutoConfiguration backs off. Sometimes, though, you
need to exclude it explicitly:
// Exclude at the application level — affects all contexts
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class, // module that has no database
SecurityAutoConfiguration.class // internal tool with no login required
})
public class MyApp { ... }
// Or via properties — useful when you can't modify the main class
# application.properties
spring.autoconfigure.exclude=\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
Add spring-boot-starter-actuator and hit
/actuator/conditions — it lists every
@AutoConfiguration candidate, whether it matched, and
which @ConditionalOn* condition decided the outcome.
Alternatively, start the app with
--debug (or logging.level.org.springframework.boot.autoconfigure=DEBUG)
to get the same report in the console as the "CONDITIONS EVALUATION
REPORT". This is the correct diagnostic tool, not trial-and-error with
exclusions.
Writing a Custom Starter
A custom starter is the right pattern when a cross-cutting concern — audit logging, a company-wide HTTP client configuration, a shared security policy — needs to be reused across multiple Spring Boot applications without each team re-implementing it. The structure follows the same two-part pattern as official starters:
/*
* Recommended layout:
*
* my-feature-spring-boot-starter/ ← empty POM, declares the autoconfigure module
* my-feature-spring-boot-autoconfigure/ ← the actual @AutoConfiguration class + conditions
*
* Naming convention: {feature}-spring-boot-starter
* Official starters use spring-boot-starter-{feature} — the reversed form is reserved for
* Spring's own starters; third-party and internal starters use your-name first.
*/
// 1. The auto-configuration class
@AutoConfiguration
@ConditionalOnClass(AuditService.class) // only if the feature is on the classpath
@ConditionalOnMissingBean(AuditService.class) // back off if the app defined its own
@EnableConfigurationProperties(AuditProperties.class)
public class AuditAutoConfiguration {
@Bean
public AuditService auditService(AuditProperties props) {
return new DefaultAuditService(props);
}
}
# 2. Register it for discovery
# src/main/resources/META-INF/spring/
# org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.audit.AuditAutoConfiguration
<!-- 3. The starter POM — no Java code, just dependency declarations -->
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>my-feature-spring-boot-autoconfigure</artifactId>
</dependency>
<!-- any other transitive dependencies the feature needs -->
</dependencies>
Without @ConditionalOnMissingBean, your starter's bean
would conflict with any application that defines its own. With it, the
auto-configured default steps aside the moment the consuming application
declares its own bean of that type — the consuming application always
wins. This is the contract that makes every official Spring Boot starter
safe to add without fear of overriding something the developer
intentionally configured.
Interview Questions
Q: What is a Spring Boot starter?
A starter is a POM-only JAR that declares a curated set of transitive
dependencies at compatible versions. It contains no Java code — its sole
purpose is to pull in the right libraries so you don't have to manually
manage each one and their version compatibility.
Q: Does adding a starter automatically configure the feature?
Not directly. The starter pulls in library JARs; those JARs ship
@AutoConfiguration classes that Spring Boot discovers via
AutoConfiguration.imports. The auto-configuration activates
conditionally — usually only if certain classes are on the classpath and
no conflicting bean is already defined.
Q: How does Spring Boot discover auto-configuration classes, and how did this change in Boot 2.7?
Before 2.7, auto-configuration class names were listed in
META-INF/spring.factories under the
EnableAutoConfiguration key. From 2.7, the preferred mechanism
is a dedicated file at
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,
one class name per line. The old spring.factories key still
works for backwards compatibility but new starters — including all official
Spring ones — use AutoConfiguration.imports.
Q: What is the role of @ConditionalOnMissingBean in a starter's auto-configuration, and what breaks without it?
It causes the auto-configured bean to register only if no bean of that
type already exists in the context. Without it, adding the starter to an
application that intentionally defines its own implementation of the same
type would create a duplicate bean conflict —
NoUniqueBeanDefinitionException at startup. With it, the
application's explicit bean takes precedence and the starter's default
silently backs off. This is the contract that makes auto-configuration
non-invasive by design.