Create a Custom Spring Boot Starter

Build a custom Spring Boot 4 starter with conditional auto-configuration, configuration properties, a consuming app, and tests for defaults and bean overrides.

A custom Spring Boot starter lets another application add a reusable feature with one dependency. The starter supplies the feature’s classes and its auto-configuration. When the application starts, Spring Boot loads that auto-configuration and creates the default beans only when its conditions match.

This example uses Spring Boot 4.1.2 and Java 17 or later. Several order applications need to format reference IDs in the same way. We will package a ReferenceIdFormatter with a configurable prefix, consume it from a separate application, and test both the default bean and an application-defined replacement.

1. Decide What the Starter Provides

The consuming application should be able to inject ReferenceIdFormatter without copying its implementation or writing a configuration class. With the default prefix, format(42) returns ORD-000042. An application can set reference-id.prefix=PAY to return PAY-000042, or provide its own ReferenceIdFormatter bean when it needs a different policy.

Part Responsibility in this example
ReferenceIdFormatter Formats an order number into a reference ID.
ReferenceIdAutoConfiguration Creates the default formatter when no application bean exists.
Starter artifact Brings the feature and required Spring Boot dependencies into one dependency.
Sample application Injects and calls the formatter.

The Spring Boot auto-configuration reference allows a simple starter to keep these parts in one module. This tutorial uses separate feature and starter modules so the dependency relationship is visible. The sample application is a third module used to verify consumption.

2. Create the Maven Modules

The root Maven project lists the modules in dependency order. The feature module contains the public API and auto-configuration; the starter module depends on it; the sample application depends only on the starter. Maven’s reactor builds them together, so the sample can use the starter without a separate publication step.

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>4.1.2</version>
  <relativePath/>
</parent>

<groupId>com.howtodoinjava</groupId>
<artifactId>custom-spring-boot-starter-example</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<properties><java.version>17</java.version></properties>
<modules>
  <module>reference-id-spring-boot</module>
  <module>reference-id-spring-boot-starter</module>
  <module>sample-app</module>
</modules>

The reference-id-spring-boot module depends on spring-boot-autoconfigure, which supplies the auto-configuration annotations, conditions and configuration-properties support. The starter’s own POM appears after the feature code so it is clear what it packages.

3. Implement the Reference ID Feature

The formatter itself is plain Java. It receives its prefix through the constructor and formats a nonnegative order number to at least six digits. Keeping the business behavior outside the configuration class means an application can also construct or replace this type directly.

public class ReferenceIdFormatter {
    private final String prefix;

    public ReferenceIdFormatter(String prefix) {
        this.prefix = prefix;
    }

    public String format(long orderId) {
        if (orderId < 0) {
            throw new IllegalArgumentException("orderId must not be negative");
        }
        return "%s-%06d".formatted(prefix, orderId);
    }
}

The configuration properties class gives the prefix a default and binds reference-id.prefix when an application supplies that property. The setter is used during binding; callers use getPrefix() after binding.

@ConfigurationProperties(prefix = "reference-id")
public class ReferenceIdProperties {
    private String prefix = "ORD";

    public String getPrefix() {
        return prefix;
    }

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }
}

4. Register Conditional Auto-Configuration

ReferenceIdAutoConfiguration is an auto-configuration class: Spring Boot discovers it from a file in the feature jar and evaluates its conditions at application startup. @ConditionalOnClass ensures that the feature class is present. @ConditionalOnMissingBean makes the default formatter back off when the application supplies a ReferenceIdFormatter bean of its own.

@AutoConfiguration
@ConditionalOnClass(ReferenceIdFormatter.class)
@EnableConfigurationProperties(ReferenceIdProperties.class)
public class ReferenceIdAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    ReferenceIdFormatter referenceIdFormatter(ReferenceIdProperties properties) {
        return new ReferenceIdFormatter(properties.getPrefix());
    }
}

Put the fully qualified class name on one line in reference-id-spring-boot/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:

com.howtodoinjava.referenceid.autoconfigure.ReferenceIdAutoConfiguration

For current Spring Boot, the .imports file is the auto-configuration registration point. The older spring.factories registration shown in some tutorials applies to older Boot versions and is not used by this example. This class is outside the sample application’s component-scan package, so its bean appears because Boot imports it from the jar.

5. Add the Starter Dependency

The reference-id-spring-boot-starter module has no Java class. Its POM collects the feature module and Spring Boot’s core starter. That is the artifact an application declares when it wants this feature.

<artifactId>reference-id-spring-boot-starter</artifactId>
<dependencies>
  <dependency>
    <groupId>com.howtodoinjava</groupId>
    <artifactId>reference-id-spring-boot</artifactId>
    <version>${project.version}</version>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
  </dependency>
</dependencies>

The complete POM files, including each module’s parent declaration, are in the source directory. A separate application would also need this starter artifact and its feature artifact in a Maven repository it can access. Running mvn install places both artifacts in the local Maven repository for local use; it does not publish them to a remote repository.

6. Use the Starter in an Application

The sample application declares the starter as its feature dependency. It does not add an @Import annotation or scan the auto-configuration package. Spring Boot reads the feature jar’s .imports file during startup.

<dependency>
  <groupId>com.howtodoinjava</groupId>
  <artifactId>reference-id-spring-boot-starter</artifactId>
  <version>${project.version}</version>
</dependency>

The application injects ReferenceIdFormatter into a CommandLineRunner and calls format() after startup:

@SpringBootApplication
public class SampleApplication {
    public static void main(String[] args) {
        SpringApplication.run(SampleApplication.class, args);
    }

    @Bean
    CommandLineRunner showReferenceId(ReferenceIdFormatter formatter) {
        return args -> System.out.println(formatter.format(42));
    }
}

With no reference-id.prefix property, the sample prints ORD-000042. Set reference-id.prefix=PAY in the consuming application’s properties to change the prefix. The formatter’s logic remains in the feature jar; only its configuration changes.

To start the sample from the repository root, first install the two library artifacts locally and then run the sample module:

mvn install -DskipTests
mvn -f sample-app/pom.xml spring-boot:run

The second command prints ORD-000042 and exits. These commands were run against the complete project; the first is needed because running the sample module on its own resolves the starter from the local Maven repository.

7. Test the Defaults and Back-Off

An ApplicationContextRunner starts a small test context around the auto-configuration. These tests check the default prefix, a configured prefix, and the condition that leaves an application-defined bean in control. The last case matters because a starter should supply a useful default without blocking an application-specific policy.

private final ApplicationContextRunner runner = new ApplicationContextRunner()
        .withConfiguration(AutoConfigurations.of(ReferenceIdAutoConfiguration.class));

@Test void usesDefaultPrefix() {
    runner.run(context -> assertThat(context.getBean(ReferenceIdFormatter.class).format(42))
            .isEqualTo("ORD-000042"));
}

@Test void bindsApplicationProperty() {
    runner.withPropertyValues("reference-id.prefix=PAY")
            .run(context -> assertThat(context.getBean(ReferenceIdFormatter.class).format(42))
                    .isEqualTo("PAY-000042"));
}

@Test void backsOffForApplicationBean() {
    runner.withUserConfiguration(UserFormatterConfiguration.class)
            .run(context -> {
                assertThat(context).hasSingleBean(ReferenceIdFormatter.class);
                assertThat(context.getBean(ReferenceIdFormatter.class).format(42))
                        .isEqualTo("CUSTOM-000042");
            });
}

UserFormatterConfiguration in the complete source declares a bean constructed with CUSTOM. A separate SampleApplicationTest starts the consuming application and verifies that Boot discovers the starter from the dependency. Run the full reactor with:

mvn test

The verified run completed four tests with no failures. It also started the sample application and printed ORD-000042. That integration test checks a different boundary from ApplicationContextRunner: the auto-configuration must be found through the actual starter dependency and .imports resource, not supplied directly to the test.

8. Frequently Asked Questions

8.1. Do I always need separate feature and starter modules?

No. Spring Boot’s current reference allows one module when auto-configuration is simple and has no optional features. Separate modules help when consumers may want the feature API without the starter’s dependency choices or when the library has optional integrations.

8.2. Why is my auto-configuration bean missing?

Check that the built feature jar contains META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports and that its line names the correct class. Then inspect the conditions: @ConditionalOnClass requires the named class, and @ConditionalOnMissingBean deliberately backs off when another formatter bean exists. The ApplicationContextRunner tests above isolate each case.

8.3. Is mvn install enough for another developer to use the starter?

Only on the machine where it ran. mvn install writes artifacts to that machine’s local Maven repository. To share the starter across machines, publish the feature and starter artifacts to a repository the consuming projects can reach, with versioning and access rules appropriate for that repository.

9. Conclusion

A custom starter combines reusable feature code, conditional auto-configuration and a dependency that applications can declare once. The .imports file makes the configuration discoverable, configuration properties let applications adjust defaults, and @ConditionalOnMissingBean lets an application replace the default bean. Test those boundaries before distributing the starter.

The complete Maven reactor and tests are in the custom Spring Boot starter project folder.

Leave a Comment

Logged in as Lokesh Gupta. Edit your profile. Log out? Required fields are marked *

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.