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@AutoConfigurationclasses,@ConfigurationProperties, and the registration fileMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importslisting 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 namedxxx-spring-boot-starter; thespring-boot-starter-*prefix is reserved for official ones.
Design rules:
- Guard everything with conditions:
@ConditionalOnClassfor optional libraries,@ConditionalOnMissingBeanon every bean so applications can replace it,@ConditionalOnPropertyfor an on/off switch. - Expose settings through a prefixed
@ConfigurationPropertiesclass and includespring-boot-configuration-processorso 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
@ConditionalOnMissingBeanso 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.