Creating Custom Spring Boot Starters: Auto-Configuration and Conditionals
Learn to build production-ready Spring Boot starters with auto-configuration and conditionals.
20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.
- ✓Java 17+ installed
- ✓Maven 3.9+ or Gradle 8+
- ✓Spring Boot 3.2+ project (any existing app will do)
- ✓Basic understanding of dependency injection and application.properties
• A Spring Boot starter is a library that bundles auto-configuration, dependencies, and property defaults so you can add a feature with a single dependency. • Use @ConditionalOnClass, @ConditionalOnProperty, and @ConditionalOnMissingBean to make auto-configuration smart and optional. • Always provide a spring.factories or org.springframework.boot.autoconfigure.AutoConfiguration.imports file to register your auto-configuration class. • Test with @SpringBootTest and custom properties to verify conditionals behave correctly in different environments. • Version your starter independently of the consuming application to avoid dependency hell.
Imagine you're building a modular kitchen appliance. A Spring Boot starter is like a pre-assembled blender attachment — you just click it onto the base (your Spring Boot app) and it works. Auto-configuration is the "smart plug" that detects whether you have fruit (a dependency) and only activates the blender if you do. Conditionals are the safety switches that prevent the blender from running if the lid is off (missing bean or property).
Spring Boot starters are the backbone of modern microservice architectures. They let you add complex functionality — from database access to messaging to security — with a single dependency in your pom.xml. But what if the starter you need doesn't exist? Or what if your organization has internal libraries that should be auto-configured? That's when you build your own. Creating a custom Spring Boot starter is a rite of passage for senior developers. It forces you to understand auto-configuration mechanics, conditional annotations, and the delicate dance between convention and configuration. In this tutorial, we'll build a production-ready starter for a SaaS billing service. You'll learn how to register auto-configuration classes, apply conditionals that respect the user's environment, and test everything like a pro. We'll also cover the pitfalls — like classpath scanning order and property binding quirks — that have caused midnight PagerDuty alerts for many of us. By the end, you'll have a reusable starter that can be dropped into any Spring Boot 3.2+ project and just work. Let's get our hands dirty.
What is a Spring Boot Starter?
A Spring Boot starter is a Maven or Gradle module that bundles together all the dependencies and auto-configuration needed to enable a feature. When you add spring-boot-starter-web to your project, you get Tomcat, Jackson, Spring MVC, and a pile of auto-configured beans. The magic happens because Spring Boot scans for META-INF/spring.factories (or the newer org.springframework.boot.autoconfigure.AutoConfiguration.imports file) to discover auto-configuration classes. These classes use conditional annotations to decide what to configure based on the classpath, properties, and existing beans. For example, spring-boot-starter-data-redis only creates a RedisConnectionFactory if you have the Lettuce or Jedis driver on the classpath. This design means starters are additive: you can stack them without conflicts. But when you build your own, you must follow the same contract. Let's build a starter for a fictional billing service called "ChargeBee Lite" that provides a PaymentGateway interface and a default implementation using Stripe.
What the Official Docs Won't Tell You
The official Spring Boot documentation tells you to use @ConditionalOnClass and spring.factories. It doesn't tell you about the silent failures when your starter's auto-configuration class is loaded but the conditionals fail silently. I've spent hours debugging a starter that worked in unit tests but not in production — the culprit was a missing transitive dependency that the conditionals didn't detect because they only check class presence, not method availability. Another hidden gotcha: property binding with @ConfigurationProperties. If your starter defines properties in a prefix like "billing.api-key", the user must use kebab-case in application.yml. But if they use camelCase (billing.apiKey), it still works — until they upgrade Spring Boot and the relaxed binding rules change. The docs also gloss over ordering. If two starters both try to configure a DataSource, the one with @AutoConfigureBefore wins. Without explicit ordering, the result is unpredictable. Finally, never rely on @ComponentScan in your starter — it defeats the purpose of auto-configuration and can cause bean duplication. Always use explicit @Bean methods with conditionals.
Registering Auto-Configuration
Spring Boot discovers auto-configuration classes via the file META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. This file (introduced in Spring Boot 2.7) replaces the older spring.factories approach. Each line contains the fully qualified name of an auto-configuration class. For backward compatibility, you can still use spring.factories, but the new method is preferred. Create the file in src/main/resources/META-INF/spring/. The order of classes in this file matters: Spring Boot processes them in order, but you can override ordering with @AutoConfigureBefore and @AutoConfigureAfter annotations. For our billing starter, we'll register BillingAutoConfiguration. If your starter depends on another auto-configuration (like DataSourceAutoConfiguration), list it after in the imports file or use @AutoConfigureAfter. A common mistake is forgetting to include this file — the starter will compile but do nothing at runtime. I've seen teams waste days wondering why their beans aren't created, only to find the imports file missing.
Conditional Annotations Deep Dive
Spring Boot provides a rich set of conditional annotations that control whether a bean or configuration class is loaded. The most common ones are @ConditionalOnClass (checks if a class is on the classpath), @ConditionalOnMissingBean (checks if a bean of a given type doesn't exist), @ConditionalOnProperty (checks if a property has a specific value), and @ConditionalOnExpression (evaluates a SpEL expression). You can combine them on the same class or method. For our billing starter, we want to enable the Stripe integration only if the Stripe SDK is present and the property 'billing.enabled' is true (default true). We also want to allow users to provide their own PaymentGateway implementation. The order of evaluation matters: @ConditionalOnClass is checked first, then @ConditionalOnProperty, then @ConditionalOnMissingBean. If any condition fails, the entire configuration class is skipped. This is important for performance — you don't want to load beans if the required library isn't even on the classpath. A common mistake is putting @ConditionalOnMissingBean on the class level instead of the method level. If you do that, the entire configuration class is skipped if the bean exists, which prevents other beans in the same class from being created.
Structuring Your Starter Project
A well-structured starter project has two modules: the autoconfigure module and the starter module. The autoconfigure module contains the auto-configuration classes, property classes, and conditionals. The starter module is an empty JAR that pulls in the autoconfigure module and the required third-party dependencies. This separation allows users to depend on the autoconfigure module directly if they want to customize the configuration. For our billing starter, the autoconfigure module includes BillingAutoConfiguration, BillingProperties, and the StripePaymentGateway implementation. The starter module depends on the autoconfigure module and the Stripe SDK. The user only needs to add the starter module to their project. This pattern is used by all official Spring Boot starters. Another best practice: keep your API interfaces (like PaymentGateway) in a separate API module that has no Spring dependencies. This allows users to implement the interface without pulling in Spring Boot. I've seen projects where the API interface was in the autoconfigure module, forcing users to depend on Spring Boot just to implement a simple interface. Don't do that.
Testing Your Custom Starter
Testing a custom starter requires a different approach than testing a regular application. You need to verify that the auto-configuration activates under the right conditions and deactivates under others. Use @SpringBootTest with a minimal application context. For our billing starter, we'll write tests that verify: 1) When Stripe SDK is on the classpath and billing.enabled is true, the PaymentGateway bean is created. 2) When Stripe SDK is missing, the bean is not created. 3) When the user provides their own PaymentGateway bean, the starter's bean is not created. To simulate missing classes, use a custom TestCondition or exclude the Stripe dependency from the test classpath. Spring Boot 3.2 introduced @ConditionalOnClass with a name attribute that accepts string class names, making it easier to test without the actual library. For property-based tests, use @TestPropertySource or @DynamicPropertySource. A common mistake is testing only the happy path and assuming conditionals work. Always test the negative cases: missing properties, missing classes, and existing beans.
Production Debugging and Monitoring
When a custom starter misbehaves in production, the first step is to determine whether the auto-configuration was applied. Enable debug logging for your starter's package by adding 'logging.level.com.example.billing.starter=DEBUG' to application.yml. This will show you which conditionals matched and which didn't. Spring Boot also exposes an autoconfigure endpoint via Actuator: GET /actuator/conditions. This endpoint shows all auto-configuration classes and their match/unmatch status. If your starter's class is listed as unmatched, the 'not matched' section will tell you why — for example, 'ConditionalOnClass did not find required class com.stripe.Stripe'. This is invaluable for debugging. Another technique: use Spring Boot's --debug flag to print the autoconfigure report at startup. I once debugged a production issue where the starter's bean was created twice — the autoconfigure report showed two matching entries because the user had accidentally included both the starter and the autoconfigure module as dependencies. The report made it obvious.
Advanced Techniques: Conditional Evaluation Order
When you have multiple auto-configuration classes in your starter, the order of evaluation matters. Use @AutoConfigureBefore and @AutoConfigureAfter to specify ordering. For example, if your billing starter needs a DataSource to store transactions, you should annotate your configuration with @AutoConfigureAfter(DataSourceAutoConfiguration.class). This ensures the DataSource is available when your beans are created. Another advanced technique is using @ConditionalOnBean with a specific bean name or type. This is useful when your starter should only activate if another starter's bean is present. For example, your billing starter might only create a webhook endpoint if Spring Security is configured. Use @ConditionalOnBean(SecurityFilterChain.class) on the webhook configuration. Be careful with circular dependencies — if two starters depend on each other's beans, you'll get a startup failure. Finally, you can create custom conditionals by implementing the Condition interface. This is useful for complex logic that can't be expressed with the built-in annotations. For example, a condition that checks the environment (dev vs prod) based on a custom property.
The Case of the Vanishing DataSource
- Always use @ConditionalOnMissingBean for beans users might already define.
- Specify auto-configuration ordering with @AutoConfigureBefore or @AutoConfigureAfter.
- Test starter behavior in an application that already defines the same beans.
jar -tf your-starter.jar | grep AutoConfigurationcurl -s http://localhost:8080/actuator/conditions | jq '.contexts.default.positiveMatches'| File | Command / Code | Purpose |
|---|---|---|
| BillingAutoConfiguration.java | @AutoConfiguration | What is a Spring Boot Starter? |
| BillingProperties.java | @ConfigurationProperties(prefix = "billing") | What the Official Docs Won't Tell You |
| AutoConfiguration.imports | com.example.billing.starter.BillingAutoConfiguration | Registering Auto-Configuration |
| ConditionalBillingAutoConfiguration.java | @AutoConfiguration | Conditional Annotations Deep Dive |
| pom.xml (starter module) | Structuring Your Starter Project | |
| BillingAutoConfigurationTest.java | class BillingAutoConfigurationTest { | Testing Your Custom Starter |
| application.yml (debug config) | logging: | Production Debugging and Monitoring |
| CustomCondition.java | public class OnProductionEnvironmentCondition implements Condition { | Advanced Techniques |
Key takeaways
Interview Questions on This Topic
Explain how Spring Boot discovers auto-configuration classes and what file is used in Spring Boot 3.x.
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.
That's Spring Boot. Mark it forged?
5 min read · try the examples if you haven't