Here’s the English translation of the article body:


Do you find yourself copy-pasting the same logging configuration or external API integration Beans into every project across your organization? It’s all too easy for one copy to get fixed while the other stays outdated. The good news is that the mechanism where “just adding a dependency activates it,” just like the official Starters, is something you can actually build yourself.

In this article, we’ll walk through the steps to promote shared code into a reusable custom Starter — from splitting modules, to generating configuration metadata, all the way to verifying activation in a separate project. The target is internal distribution via an internal repository; we won’t cover publishing to Maven Central.

The basics of Starters from the consumer side are covered in [[what-is-spring-boot-starter]], and the internal mechanism that makes AutoConfiguration work is in [[spring-boot-autoconfiguration-mechanism-explained]]. Here, we focus on the producer side.

Split Into Two Modules

By official convention, a Starter is split into two modules.

  • xxx-spring-boot-autoconfigure … the actual auto-configuration (classes and conditions)
  • xxx-spring-boot-starter … a thin aggregation module that just bundles dependencies

Why split them? To accommodate the case where someone doesn’t want the auto-configuration but does want to use just the library itself. By depending on the autoconfigure side directly they can use only the Beans, while depending on the starter side gives them the fully wired, auto-activated setup. This leaves both options open to your users.

Naming matters too. Names starting with spring-boot- are reserved by Spring’s official project. For your own, use the xxx-spring-boot-starter format with your own prefix at the front, like acme-spring-boot-starter.

The directory layout looks like this.

acme-notify/
├── acme-notify-spring-boot-autoconfigure/   # the actual auto-configuration
│   └── src/main/...
└── acme-notify-spring-boot-starter/          # just bundles dependencies
    └── pom.xml

The autoconfigure Module Skeleton

Here’s the pom.xml for the autoconfigure side. It depends on spring-boot-autoconfigure, and includes spring-boot-configuration-processor for metadata generation as optional. Making the target integration library optional as well means it won’t be dragged in when it’s absent from the consumer’s classpath.

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
  </dependency>
  <!-- 連携対象のライブラリ(例)も optional に -->
  <dependency>
    <groupId>com.acme</groupId>
    <artifactId>notify-client</artifactId>
    <optional>true</optional>
  </dependency>
</dependencies>

The starter side’s pom.xml simply pulls in the autoconfigure module and the necessary real dependencies transitively.

<dependencies>
  <dependency>
    <groupId>com.acme</groupId>
    <artifactId>acme-notify-spring-boot-autoconfigure</artifactId>
    <version>${project.version}</version>
  </dependency>
  <dependency>
    <groupId>com.acme</groupId>
    <artifactId>notify-client</artifactId>
  </dependency>
</dependencies>

As our subject, we’ll proceed on the assumption of providing a NotifyClient — which calls an external notification API — as a shared Bean.

Write the Auto-Configuration Class with @AutoConfiguration

Annotate the auto-configuration class with @AutoConfiguration. Introduced in Spring Boot 2.7, it’s an auto-configuration-specific annotation that internally is equivalent to @Configuration(proxyBeanMethods = false).

Let’s add the conditional annotations at the same time.

@AutoConfiguration
@ConditionalOnClass(NotifyClient.class)
@EnableConfigurationProperties(NotifyProperties.class)
public class NotifyAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(prefix = "acme.notify", name = "enabled", matchIfMissing = true)
    public NotifyClient notifyClient(NotifyProperties props) {
        return new NotifyClient(props.getEndpoint(), props.getApiKey());
    }
}

Here’s what each condition means.

  • @ConditionalOnClass … configure only when NotifyClient is on the classpath
  • @ConditionalOnMissingBean … if the user defines their own Bean, give theirs priority and don’t override it
  • @ConditionalOnProperty … can be disabled with acme.notify.enabled=false (enabled by default)

This @ConditionalOnMissingBean is quietly important. Forget it, and you’ll silently clobber the user’s customized NotifyClient. A custom Starter should be designed to “stay out of the way” as a baseline.

Register It in AutoConfiguration.imports

Just adding @AutoConfiguration isn’t enough for Spring Boot to find the class. You need a registration file. Under src/main/resources in the autoconfigure module, create the following file.

src/main/resources/META-INF/spring/
  org.springframework.boot.autoconfigure.AutoConfiguration.imports

The contents are simply fully qualified class names, one class per line.

com.acme.notify.autoconfigure.NotifyAutoConfiguration

This is the mechanism that, from Spring Boot 2.7 onward, replaces the old EnableAutoConfiguration key in spring.factories. Older articles show the procedure of writing to META-INF/spring.factories, but this imports file is the correct approach now. The number one cause of auto-configuration not taking effect is forgetting to place this file or getting the path wrong, so suspect this first.

Bind Settings with @ConfigurationProperties

Since we want to receive external configuration in a type-safe way, let’s prepare a properties class. The trick is to base the prefix on the library name so it doesn’t collide with other libraries.

@ConfigurationProperties(prefix = "acme.notify")
public class NotifyProperties {

    /** 通知APIのエンドポイントURL。 */
    private String endpoint = "https://api.acme.example/notify";

    /** 認証に使うAPIキー。 */
    private String apiKey;

    // getter / setter は省略
}

The basics of binding are summarized in [[spring-boot-properties-configuration-guide]], and input validation using @Validated is covered in [[spring-boot-configuration-properties-validation-guide]], so be sure to check those out as well.

Enable IDE Completion with the Metadata JSON

The spring-boot-configuration-processor we added earlier as optional auto-generates META-INF/spring-configuration-metadata.json at build time. With this in place, completion and descriptions become available in the consumer’s application.yml.

The generated contents look like this.

{
  "properties": [
    {
      "name": "acme.notify.endpoint",
      "type": "java.lang.String",
      "description": "通知APIのエンドポイントURL。",
      "defaultValue": "https://api.acme.example/notify"
    }
  ]
}

What’s worth noting is the description. It’s a direct reflection of the Javadoc comment written on the property’s field. In other words, if you write thorough Javadoc, it shows up as hints in the user’s IDE as-is. Since the documentation and code stay in sync, there’s no downside to writing it.

When you want to supplement items that the processor can’t pick up — such as constructor arguments — you can hand-write META-INF/additional-spring-configuration-metadata.json.

The consumer’s application.yml can be written like this.

acme:
  notify:
    enabled: true
    endpoint: https://api.acme.example/notify
    api-key: ${NOTIFY_API_KEY}

When You Need Auto-Configuration Ordering

When you depend on other auto-configurations, specify the order. For example, if you want to run after the DataSource is configured, place yourself later using after.

@AutoConfiguration(after = DataSourceAutoConfiguration.class)
public class NotifyAutoConfiguration {
    // ...
}

Conversely, if you want to run before other configurations, use before. Get the order wrong, and initialization runs while a dependent Bean doesn’t yet exist, and it fails. That said, the situations that actually call for this in practice are limited, so at first it’s enough to “specify it minimally, only when you depend on another configuration.”

Verify Activation in a Separate Project

Finally, let’s confirm that it really works just by adding the dependency. First, install the autoconfigure and starter locally.

mvn install

If you’re pushing to your organization’s internal repository, use mvn deploy, but for a simple sanity check, a local mvn install is enough. In the verification project’s pom.xml, add only the starter module.

<dependency>
  <groupId>com.acme</groupId>
  <artifactId>acme-notify-spring-boot-starter</artifactId>
  <version>1.0.0</version>
</dependency>

If NotifyClient can be injected with just this, you’ve succeeded. When you want a detailed look at the application status, starting up with --debug produces a Conditions Evaluation Report.

java -jar app.jar --debug

If NotifyAutoConfiguration appears under Positive matches in the report, it’s been activated. If it doesn’t appear, check the following three points in order from the top.

  • Did you write the class name in AutoConfiguration.imports, and is the path correct?
  • Is the target class of @ConditionalOnClass on the classpath?
  • Is the spelling of @ConditionalOnProperty and the prefix correct?

Summary

Looking back at the flow of building a custom Starter, it was a straight line: split into two modules → @AutoConfiguration → imports registration → conditional configuration → properties → metadata generation → verification. Once you have the template down, you can roll out the same pattern to other shared features.

The key is to respect the user’s overrides with @ConditionalOnMissingBean, and to design the prefix so it’s collision-resistant, making it a “Starter that stays out of the way.” Graduating from copy-paste operations means fixing shared code in just one place, and the inconsistencies between projects disappear. As a first step toward internal infrastructure, try promoting the configuration you copy-paste most often first.


I tried to write this to src/content/blog/en/spring-boot-custom-starter-creation-guide.md (with translated frontmatter matching your existing EN files) but the write needs your permission. Let me know if you’d like me to save it there — the file doesn’t exist yet.