Writing your own starter

Spring Boot · Expert · 16 min

Platform teams share logging, security and client setup across dozens of services through internal starters.

When several services need the same setup (an HTTP client for an internal API, audit logging, a security baseline), package it as a starter so each service adds one dependency and gets sensible defaults it can override.

The conventional structure is two modules:

  • acme-audit-spring-boot-autoconfigure: the @AutoConfiguration classes, @ConfigurationProperties, and the registration file META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports listing them, one fully qualified class name per line.
  • acme-audit-spring-boot-starter: an empty jar whose POM depends on the autoconfigure module and the libraries it needs. Third-party starters are named xxx-spring-boot-starter; the spring-boot-starter-* prefix is reserved for official ones.

Design rules:

  • Guard everything with conditions: @ConditionalOnClass for optional libraries, @ConditionalOnMissingBean on every bean so applications can replace it, @ConditionalOnProperty for an on/off switch.
  • Expose settings through a prefixed @ConfigurationProperties class and include spring-boot-configuration-processor so IDEs auto-complete them.
  • Don't component-scan inside auto-configuration; declare beans explicitly. Use @AutoConfiguration(after = ...) to order relative to Boot's own.

Test it with ApplicationContextRunner, which starts tiny contexts in milliseconds and lets you assert which beans exist under different properties and classpaths (FilteredClassLoader).

Example

// autoconfigure module
@ConfigurationProperties("acme.audit")
public record AuditProperties(@DefaultValue("true") boolean enabled,
                              @DefaultValue("audit") String topic) {}

@AutoConfiguration
@ConditionalOnClass(AuditClient.class)
@ConditionalOnProperty(prefix = "acme.audit", name = "enabled", matchIfMissing = true)
@EnableConfigurationProperties(AuditProperties.class)
public class AuditAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    AuditClient auditClient(AuditProperties props) { return new AuditClient(props.topic()); }
}
// META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
//   contains one line: com.acme.audit.AuditAutoConfiguration

class AuditAutoConfigurationTest {
    private final ApplicationContextRunner runner = new ApplicationContextRunner()
        .withConfiguration(AutoConfigurations.of(AuditAutoConfiguration.class));

    @Test void createsClient() { runner.run(ctx -> assertThat(ctx).hasSingleBean(AuditClient.class)); }
    @Test void canBeDisabled() { runner.withPropertyValues("acme.audit.enabled=false")
        .run(ctx -> assertThat(ctx).doesNotHaveBean(AuditClient.class)); }
}

Remember

  • Split into an autoconfigure module and an empty starter POM
  • Register classes in AutoConfiguration.imports; don't rely on scanning
  • Make every bean @ConditionalOnMissingBean so apps can override
  • Test with ApplicationContextRunner

Watch out for

  • Naming an internal starter spring-boot-starter-foo, the prefix reserved for official Spring Boot starters.

Check yourself

  1. How does Spring Boot discover your auto-configuration class in a starter jar?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.spring.io/spring-boot/reference/features/developing-auto-configuration.html

Builds on

Unlocks

Nothing builds on this yet.