Spring Boot Tutorial
Creating a Custom Spring Boot Starter
Large organisations often repeat the same configuration in every service: an audit client, a standard security setup, company-wide logging, a client for an internal API. Copying that code leads to drift and bugs. The Spring Boot way to share it is to build your own starter — a dependency that brings the right libraries and auto-configures beans, exactly like the official starters do.
This lesson builds a complete starter step by step: the auto-configuration class, conditions, configuration properties with IDE metadata, the registration file, the starter module, and tests with ApplicationContextRunner.
Anatomy of a Starter
By convention a starter has two modules. The autoconfigure module (acme-audit-spring-boot-autoconfigure) contains @AutoConfiguration classes and @ConfigurationProperties. The starter module (acme-audit-spring-boot-starter) is an almost empty POM that depends on the autoconfigure module and the libraries it needs. For small internal starters, a single module is fine. Do not name your artifacts spring-boot-*; that prefix is reserved for official Spring Boot modules.
Auto-Configuration Classes and Conditions
An @AutoConfiguration class is a @Configuration that Spring Boot loads only if it is listed in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. Conditions decide whether each bean is created:
@ConditionalOnClass— only if a class is on the classpath.@ConditionalOnMissingBean— only if the user has not defined their own bean; this is what makes your defaults overridable.@ConditionalOnProperty— only if a property has a certain value, e.g.acme.audit.enabled=true.@ConditionalOnBean,@ConditionalOnWebApplication,@ConditionalOnResourceand more.@AutoConfiguration(after = ...)— order relative to other auto-configurations.
Configuration Properties and Metadata
Expose settings with a @ConfigurationProperties record and enable it with @EnableConfigurationProperties on the auto-configuration. Add the spring-boot-configuration-processor annotation processor so the build generates META-INF/spring-configuration-metadata.json; IDEs then offer auto-completion and documentation for your properties in application.yml.
Testing with ApplicationContextRunner
ApplicationContextRunner starts tiny application contexts in milliseconds, with chosen auto-configurations, user configurations and properties. You can assert which beans exist, that user beans win over defaults, and that bad configuration fails. Use FilteredClassLoader to simulate a library being absent.
Examples
The library code the starter will configure
package com.acme.audit;
public class AuditClient {
private final String serviceName;
private final URI endpoint;
private final boolean async;
public AuditClient(String serviceName, URI endpoint, boolean async) {
this.serviceName = serviceName;
this.endpoint = endpoint;
this.async = async;
}
public void record(String action, String user) {
System.out.printf("[audit %s -> %s async=%s] %s by %s%n", serviceName, endpoint, async, action, user);
}
}
(Plain Java class — the starter's job is to create and configure it automatically.)
Properties, auto-configuration and registration file
@ConfigurationProperties(prefix = "acme.audit")
public record AuditProperties(
/** Whether auditing is enabled. */
@DefaultValue("true") boolean enabled,
/** Audit collector URL. */
@DefaultValue("http://audit.acme.internal/events") URI endpoint,
/** Send events asynchronously. */
@DefaultValue("true") boolean async) {}
@AutoConfiguration
@ConditionalOnClass(AuditClient.class)
@ConditionalOnProperty(prefix = "acme.audit", name = "enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(AuditProperties.class)
public class AuditAutoConfiguration {
@Bean
@ConditionalOnMissingBean // users can replace it with their own bean
AuditClient auditClient(AuditProperties props, Environment env) {
String service = env.getProperty("spring.application.name", "unknown-service");
return new AuditClient(service, props.endpoint(), props.async());
}
}
# src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.acme.audit.autoconfigure.AuditAutoConfiguration
(Any application that adds the starter gets an AuditClient bean automatically.)
Starter POM and usage in an application
<!-- acme-audit-spring-boot-starter/pom.xml -->
<artifactId>acme-audit-spring-boot-starter</artifactId>
<dependencies>
<dependency>
<groupId>com.acme</groupId>
<artifactId>acme-audit-spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>com.acme</groupId>
<artifactId>acme-audit-client</artifactId>
</dependency>
</dependencies>
<!-- autoconfigure module: generate IDE metadata -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
// In a consuming application — nothing to configure except optional properties
@Service
public class RefundService {
private final AuditClient audit;
public RefundService(AuditClient audit) { this.audit = audit; }
public void refund(String orderId, String user) {
audit.record("REFUND " + orderId, user);
}
}
[audit orders-service -> http://audit.acme.internal/events async=true] REFUND WN-10231 by ravi@webnest.in
Testing the auto-configuration with ApplicationContextRunner
class AuditAutoConfigurationTest {
private final ApplicationContextRunner runner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(AuditAutoConfiguration.class));
@Test
void createsClientByDefault() {
runner.run(ctx -> assertThat(ctx).hasSingleBean(AuditClient.class));
}
@Test
void canBeDisabled() {
runner.withPropertyValues("acme.audit.enabled=false")
.run(ctx -> assertThat(ctx).doesNotHaveBean(AuditClient.class));
}
@Test
void userBeanWins() {
runner.withBean("custom", AuditClient.class,
() -> new AuditClient("custom", URI.create("http://localhost"), false))
.run(ctx -> assertThat(ctx).getBean(AuditClient.class)
.isSameAs(ctx.getBean("custom")));
}
@Test
void backsOffWhenLibraryIsMissing() {
runner.withClassLoader(new FilteredClassLoader(AuditClient.class))
.run(ctx -> assertThat(ctx).doesNotHaveBean("auditClient"));
}
}
AuditAutoConfigurationTest
✔ createsClientByDefault()
✔ canBeDisabled()
✔ userBeanWins()
✔ backsOffWhenLibraryIsMissing()
(all four run in under a second)
Common Mistakes
- Annotating auto-configuration classes with @Component or putting them in a package scanned by the application, so conditions and ordering are bypassed.
- Forgetting @ConditionalOnMissingBean, making it impossible for applications to override your defaults.
- Listing auto-configurations in the old spring.factories file, which Spring Boot 3+ no longer reads for auto-configuration.
- Naming your artifact spring-boot-starter-xyz, which is reserved for official starters.
- Skipping the configuration processor, leaving users without IDE auto-completion for your properties.
Key Points to Remember
- A starter = autoconfigure module (@AutoConfiguration + properties) + starter POM that pulls dependencies.
- Register auto-configurations in META-INF/spring/...AutoConfiguration.imports.
- Use conditions — especially @ConditionalOnMissingBean — so defaults back off gracefully.
- Expose settings with @ConfigurationProperties and generate metadata with the configuration processor.
- Test auto-configurations quickly with ApplicationContextRunner.
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.