Spring Boot Starters Deep Dive: How They Work & Create Custom Starters
Master Spring Boot starters—auto-configuration internals, conditional beans, and custom starter creation.
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
- ✓Working knowledge of Spring Boot (see spring-boot-introduction)
- ✓Understanding of auto-configuration (see spring-boot-auto-configuration)
- ✓Familiarity with Maven or Gradle dependency management
- ✓Java 17+ and Spring Boot 3.2.x (or any 3.x)
- ✓Experience with @Configuration and @Bean annotations
• Spring Boot starters are pre-packaged dependency sets that auto-configure beans via @EnableAutoConfiguration • Auto-configuration classes are loaded from spring.factories and activated by @Conditional annotations • Custom starters require a spring.factories file, an auto-configuration class, and a properties binding class • Use @ConditionalOnClass, @ConditionalOnMissingBean, and @ConditionalOnProperty to control when auto-configuration applies • Production starters must handle graceful degradation, health indicators, and externalized configuration via @ConfigurationProperties
Think of Spring Boot starters like a pre-assembled toolkit for a specific job. If you want to build a bookshelf, you don't hunt for a hammer, screws, and wood separately—you grab a "bookshelf starter kit" that includes everything, with instructions (auto-configuration) to assemble it. The starter knows when you already have a hammer (conditional on missing bean) and won't give you a second one. If you're missing a screwdriver (missing class), it skips that step. Custom starters let you create your own kits for your team's common needs.
Spring Boot starters are the backbone of rapid application development in the Spring ecosystem. They eliminate the tedious boilerplate of dependency management and configuration by bundling related libraries and providing sensible defaults. But beneath the surface, starters are a sophisticated mechanism leveraging auto-configuration, conditional beans, and property binding. In this article, we'll dissect how starters work from the inside out, covering the exact mechanisms Spring Boot uses to load and apply auto-configuration classes. We'll explore the spring.factories file, the @EnableAutoConfiguration annotation chain, and the hierarchy of @Conditional annotations that control bean creation. Then we'll build a production-grade custom starter step-by-step, including a health indicator, metrics, and external configuration. We'll also cover debugging techniques using --debug and the auto-configuration report, common pitfalls like bean override conflicts, and how to handle versioning. By the end, you'll be able to create starters that behave as seamlessly as the official ones, and you'll understand why a misconfigured starter can silently break your application. This is advanced territory—you should already know Spring Boot basics (see spring-boot-introduction and spring-boot-auto-configuration). We'll reference real version numbers: Spring Boot 3.2.x, Java 17+.
How Spring Boot Starters Work Under the Hood
At its core, a Spring Boot starter is a Maven POM (or Gradle module) that declares a set of dependencies and includes an auto-configuration class. When you add a starter like spring-boot-starter-web to your project, Maven pulls in spring-web, spring-webmvc, tomcat-embed-core, and jackson-databind. But the real magic happens at startup. Spring Boot scans the classpath for META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (or the legacy spring.factories) files. This file lists all auto-configuration classes that Spring Boot should consider. For example, spring-boot-autoconfigure contains entries like org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration. Spring Boot's AutoConfigurationImportSelector reads these entries and passes them to the configuration phase. Each auto-configuration class is annotated with @Configuration and one or more @Conditional annotations. The conditions are evaluated: @ConditionalOnClass checks if a specific class is on the classpath (e.g., DispatcherServlet triggers WebMvcAutoConfiguration). @ConditionalOnMissingBean ensures the auto-configuration only runs if the user hasn't already defined a bean of the same type. @ConditionalOnProperty allows enabling/disabling via properties like spring.autoconfigure.exclude. The evaluation order matters: auto-configurations are sorted using @AutoConfigureOrder and @AutoConfigureBefore/@AutoConfigureAfter. For example, DataSourceAutoConfiguration must run before HibernateJpaAutoConfiguration. If a condition fails, the entire auto-configuration class is skipped, and no beans from that class are registered. This is why removing a dependency like spring-boot-starter-data-jpa from your classpath instantly disables JPA auto-configuration—the @ConditionalOnClass(DataSource.class) fails. Understanding this mechanism is crucial for debugging why a starter didn't apply or why beans are missing.
--debug or set logging.level.org.springframework.boot.autoconfigure=DEBUG to see why conditions matched or failed. I've seen teams waste days debugging missing beans when a simple condition check revealed the issue.What the Official Docs Won't Tell You
The official Spring Boot documentation covers the basics of starters and auto-configuration, but it glosses over several critical details that bite you in production. First, the spring.factories file format is case-sensitive and whitespace-sensitive. A trailing space after a class name can cause the entire entry to be silently ignored. I've seen this happen when copying from documentation. Second, auto-configuration classes are loaded lazily by default in Spring Boot 2.2+, but only if you have spring-boot-starter-webflux or similar reactive stacks. For traditional servlet stacks, they're eager. This can cause subtle timing issues if your starter depends on beans from another auto-configuration that hasn't been processed yet. Third, the @ConditionalOnClass annotation evaluates class presence using the classloader of the auto-configuration class, not the application classloader. If your starter is in a separate JAR with a different classloader (e.g., in a shared library), the condition might fail even if the class is available in the app. Fourth, property binding with @ConfigurationProperties requires a getter and setter for each property, but Spring Boot 3.x also supports record-based binding. However, if you use records, you lose the ability to mutate properties after construction, which can break some libraries. Fifth, the auto-configuration report generated by --debug shows only the final decision (matched or failed), not the reason for failure. To get the full condition evaluation log, you need to set logging.level.org.springframework.boot.autoconfigure.condition=DEBUG. Finally, many developers don't realize that you can exclude auto-configurations via spring.autoconfigure.exclude in application.properties, but this only works if the auto-configuration class is on the classpath. If you want to exclude a starter entirely, you need to exclude its dependency from your build tool.
Anatomy of a Custom Starter: Project Structure and Dependencies
A custom Spring Boot starter typically consists of two modules: an autoconfigure module and a starter module. The autoconfigure module contains the auto-configuration class, configuration properties, and any custom beans. The starter module is just a POM that depends on the autoconfigure module and any required third-party libraries. This separation allows users to depend on the starter and get all dependencies, while also allowing advanced users to depend only on the autoconfigure module if they want to manage dependencies themselves. The naming convention is important: the starter module should be named something like mylibrary-spring-boot-starter, and the autoconfigure module should be mylibrary-spring-boot-autoconfigure. The autoconfigure module should not depend on the starter. For our example, we'll create a starter for a fictional payment gateway called "PayFast". The project structure looks like this: payfast-spring-boot-autoconfigure/ (contains auto-config, properties, health indicator) and payfast-spring-boot-starter/ (contains only pom.xml). The autoconfigure module's pom.xml must include spring-boot-autoconfigure as a compile dependency (scope provided if you want to avoid transitive dependency issues). It should also include spring-boot-starter for the base starter dependencies. For the properties class, we use @ConfigurationProperties with a prefix like "payfast.api". The auto-configuration class uses @ConditionalOnClass to check for PayFastClient.class, @ConditionalOnProperty to enable/disable via "payfast.enabled", and @ConditionalOnMissingBean to allow users to override. The spring.factories file in META-INF/spring/ (or the AutoConfiguration.imports file for Spring Boot 2.7+) lists the auto-configuration class. For Spring Boot 3.x, use META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports with one class name per line.
Creating Configuration Properties for Your Starter
Configuration properties are the cornerstone of a good starter. They allow users to customize behavior without modifying code. In Spring Boot, you use @ConfigurationProperties with a prefix to bind properties from application.properties or application.yml to a POJO. For our PayFast starter, we want properties like payfast.api-key, payfast.base-url, payfast.timeout, and payfast.enabled. The properties class must have getters and setters (or be a record in Spring Boot 3.x). It's also good practice to provide sensible defaults. For example, set timeout to 5000ms and base-url to a production URL. You should also add validation using Jakarta Bean Validation annotations like @NotBlank on api-key. The @EnableConfigurationProperties annotation on the auto-configuration class activates the binding. One advanced technique is to use @ConfigurationProperties with a nested class for grouping, like payfast.retry.max-attempts and payfast.retry.backoff. This makes the properties hierarchical and easier to manage. Another consideration is metadata generation. If you include spring-boot-configuration-processor as an annotation processor in your autoconfigure module, it generates a spring-configuration-metadata.json file. This gives users IDE autocompletion when editing application.properties. To add this, include the dependency with scope annotationProcessor. The processor scans classes annotated with @ConfigurationProperties and generates metadata. You can also add custom hints using @JsonPropertyDescription or by creating an additional-spring-configuration-metadata.json file. Finally, be careful with property relaxation: Spring Boot 3.x uses a relaxed binding that allows kebab-case, camelCase, and underscore variants. But if you have properties with the same name in different cases, it can cause ambiguity. Stick to kebab-case in properties files and camelCase in Java fields.
Adding Health Indicators and Metrics to Your Starter
A production-grade starter must provide observability. Spring Boot Actuator makes it easy to add health indicators and metrics. A health indicator allows monitoring systems to check if your component is functioning correctly. For our PayFast starter, we can create a health indicator that pings the PayFast API's health endpoint. The indicator should implement the HealthIndicator interface and override the health() method. Return Health.up() if the API responds, Health.down() with a detail message if it fails. You can also include custom data like latency or last check time. To register the health indicator, simply declare it as a @Bean in your auto-configuration. Spring Boot automatically discovers all beans implementing HealthIndicator and aggregates them under the /actuator/health endpoint. For metrics, you can use Micrometer, which is included in Spring Boot 2.x and 3.x. Create a MeterBinder bean that binds your custom metrics, such as a counter for API calls, a timer for request duration, or a gauge for connection pool size. For example, you can use MeterRegistry to register a counter named "payfast.api.calls" and increment it each time the client makes a request. This integrates with Prometheus, Datadog, or any Micrometer-compatible monitoring system. You can also expose custom metrics via @Timed annotations on methods if you use AspectJ. Another important aspect is to provide a health contributor that respects the application's liveness and readiness probes. In Kubernetes, you can map the /actuator/health/liveness and /actuator/health/readiness endpoints. Your starter should include a custom readiness indicator that checks if the PayFast client is initialized and connected. Finally, ensure your health indicator is resilient: it should have a timeout and not block the entire health check if the external service is slow. Use a separate thread pool or a timeout wrapper.
Testing Your Custom Starter
Testing a custom starter requires a different approach than testing a regular application. You need to verify that auto-configuration applies correctly under various conditions. The key is to use Spring Boot's testing support with @SpringBootTest and a test-specific application context. For unit testing the auto-configuration logic, you can use the @ConditionalOnClass and @ConditionalOnMissingBean conditions by manipulating the classpath. However, the most effective approach is to create integration tests that simulate different scenarios. For example, you can create a test that loads only your auto-configuration with a minimal context using @SpringBootTest(classes = PayFastAutoConfiguration.class) and then assert that the PayFastClient bean is created. To test conditions, you can use @TestPropertySource to set properties like payfast.enabled=false and verify the bean is absent. You can also use @MockBean to simulate missing dependencies. A powerful technique is to use the ApplicationContextRunner from Spring Boot's test utilities. This runner allows you to create a minimal application context with specific conditions and verify the beans that are created. For example, you can create a runner with a custom classloader that excludes a certain class to test @ConditionalOnClass failure. You should also test that your properties are bound correctly. Use @ConfigurationPropertiesBindHandler to validate binding. For health indicators, use MockWebServer (from OkHttp) to simulate the external API. Finally, test that your starter integrates correctly with other starters. For example, if your starter depends on a DataSource, test that it works with spring-boot-starter-data-jpa. Use a test slice like @JdbcTest to avoid loading the entire application. Remember to test both success and failure scenarios. A common mistake is to only test the happy path, leaving production failures undetected.
Debugging Auto-Configuration Issues
When a custom starter doesn't work as expected, you need systematic debugging. The first tool is the auto-configuration report. Run your application with --debug or set logging.level.org.springframework.boot.autoconfigure=DEBUG. This prints a report showing which auto-configuration classes were considered, whether their conditions matched, and why some failed. For example, you might see "DataSourceAutoConfiguration matched due to @ConditionalOnClass found required classes 'javax.sql.DataSource', 'org.h2.Driver'" or "FlywayAutoConfiguration failed due to @ConditionalOnClass missing 'org.flywaydb.core.Flyway'". This report is invaluable. However, it only shows the final outcome, not intermediate steps. To see the full condition evaluation, set logging.level.org.springframework.boot.autoconfigure.condition=TRACE. This logs every condition check, including the classloader and resource location. Another common issue is bean override conflicts. If your starter creates a bean that the user also defines, you'll get a BeanDefinitionOverrideException unless you set spring.main.allow-bean-definition-overriding=true. But this is a bad practice; instead, use @ConditionalOnMissingBean to let the user's bean take precedence. If your starter depends on another auto-configuration, ensure the ordering is correct. Use @AutoConfigureAfter or @AutoConfigureBefore to specify dependencies. For example, if your starter needs a DataSource, add @AutoConfigureAfter(DataSourceAutoConfiguration.class). You can also use the @AutoConfigureOrder annotation to set a relative order. Another debugging technique is to use the Actuator's /actuator/conditions endpoint (if spring-boot-starter-actuator is included). This endpoint exposes the same information as the debug report but in JSON format, which is easier to parse programmatically. Finally, if your starter is not being loaded at all, check the spring.factories or AutoConfiguration.imports file location. The file must be in META-INF/spring/ and the class names must be fully qualified. A missing newline at the end of the file can cause the last entry to be ignored.
Best Practices for Production-Ready Starters
Creating a starter that works in development is easy; making it production-ready requires discipline. First, always provide sensible defaults that work out of the box, but allow full customization via properties. Never hardcode values that might vary between environments, like URLs or timeouts. Second, use @ConditionalOnMissingBean for every bean you create, so users can replace your implementation with their own. This is especially important for beans like RestTemplate, ObjectMapper, or your custom client. Third, implement graceful degradation. If the external service your starter wraps is down, your starter should not crash the entire application. Use fallback mechanisms, circuit breakers (via Resilience4j), or at least log a warning and return a default value. Fourth, provide comprehensive observability: health indicators, metrics, and logging. Use structured logging (like JSON) to make logs machine-parseable. Fifth, version your starter properly. Use semantic versioning and document breaking changes. If you change the property prefix or remove a bean, bump the major version. Sixth, document your starter. Include a README with examples, property reference, and troubleshooting guide. Consider generating a Spring Boot configuration metadata file for IDE support. Seventh, test your starter with different versions of Spring Boot. Use a compatibility matrix in your CI. Eighth, avoid transitive dependency hell. Use the 'provided' scope for spring-boot-autoconfigure and other Spring Boot libraries. Pin versions of third-party libraries using Maven BOMs. Ninth, consider providing a configuration processor for custom annotations if your starter uses them. Finally, think about security. If your starter handles credentials (like API keys), use Spring Cloud Config or a secrets manager instead of plain text properties. Use @ConfigurationProperties with validation to ensure required secrets are provided.
The Starter That Silently Broke Our Database Connection Pool
- Always use @ConditionalOnMissingBean for beans that users might want to customize.
- Respect existing Spring Boot properties; use @ConfigurationProperties to bind them.
- Add health indicators to custom starters to detect resource exhaustion early.
- Test starters in a production-like environment with high concurrency before release.
grep -r "PayFastAutoConfiguration" target/classes/META-INF/spring/java -jar myapp.jar --debug | grep -i "payfast"| File | Command / Code | Purpose |
|---|---|---|
| AutoConfigurationImports.java | public class AutoConfigurationImports { | How Spring Boot Starters Work Under the Hood |
| ConditionEvaluationReportExample.java | @Component | What the Official Docs Won't Tell You |
| PayFastAutoConfiguration.java | @AutoConfiguration | Anatomy of a Custom Starter |
| PayFastProperties.java | @ConfigurationProperties(prefix = "payfast") | Creating Configuration Properties for Your Starter |
| PayFastHealthIndicator.java | @Component | Adding Health Indicators and Metrics to Your Starter |
| PayFastAutoConfigurationTest.java | class PayFastAutoConfigurationTest { | Testing Your Custom Starter |
| DebuggingCommands.java | @Component | Debugging Auto-Configuration Issues |
| PayFastStarterBestPractices.java | @Bean | Best Practices for Production-Ready Starters |
Key takeaways
Interview Questions on This Topic
Explain the role of spring.factories (or AutoConfiguration.imports) in Spring Boot starters.
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
That's Spring Boot. Mark it forged?
8 min read · try the examples if you haven't