Spring Boot starters are a powerful mechanism for packaging and distributing reusable configurations, but understanding how they actually inject those configurations can feel like magic.

Let’s see it in action. Imagine we have a simple service that needs a MessageService.

// src/main/java/com/example/myservice/MyService.java
package com.example.myservice;

public class MyService {

    private final String message;

    public MyService(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

Now, we want to create a starter that automatically configures this MyService when it’s on the classpath.

First, we need to define our auto-configuration class. This class will contain the logic to create our MyService bean.

// src/main/java/com/example/myautoconfigure/MyAutoConfiguration.java
package com.example.myautoconfigure;

import com.example.myservice.MyService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties(MyServiceProperties.class) // Enables binding properties
@ConditionalOnProperty(name = "my.service.enabled", havingValue = "true", matchIfMissing = true) // Only if property is set or missing
public class MyAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean // Only create if no other MyService bean exists
    public MyService myService(MyServiceProperties properties) {
        return new MyService(properties.getMessage());
    }
}

Notice the @ConditionalOnProperty and @ConditionalOnMissingBean annotations. These are crucial for controlling when this auto-configuration is applied. @ConditionalOnProperty ensures our starter only activates if a specific property (my.service.enabled) is set to true (or not set at all, thanks to matchIfMissing = true). @ConditionalOnMissingBean prevents our auto-configuration from overriding a MyService bean that a user might have explicitly defined.

We also need a MyServiceProperties class to bind external configuration.

// src/main/java/com/example/myautoconfigure/MyServiceProperties.java
package com.example.myautoconfigure;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("my.service")
public class MyServiceProperties {

    private String message = "Default Message"; // Default value

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

This MyServiceProperties class will allow users to configure the message via application.properties or application.yml using the prefix my.service.

To make Spring Boot discover our auto-configuration, we need to create a META-INF/spring.factories file in our starter project.

# src/main/resources/META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.myautoconfigure.MyAutoConfiguration

This file tells Spring Boot which auto-configuration classes to load.

Now, in a separate application that depends on our starter (let’s call it my-starter and my-app), we can simply add the starter as a dependency.

<!-- my-app's pom.xml -->
<dependency>
    <groupId>com.example</groupId>
    <artifactId>my-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

When my-app starts, Spring Boot will scan its classpath, find META-INF/spring.factories, and load MyAutoConfiguration. Because my.service.enabled is likely true by default (or not set, triggering matchIfMissing), and assuming no other MyService bean is defined, Spring Boot will execute the @Bean method in MyAutoConfiguration, creating a MyService bean.

We can then inject and use it:

// src/main/java/com/example/myapp/MyApplication.java
package com.example.myapp;

import com.example.myservice.MyService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class MyApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(MyApplication.class, args);

        MyService myService = context.getBean(MyService.class);
        System.out.println("Message from MyService: " + myService.getMessage());

        context.close();
    }
}

If we add my.service.message=Hello from Application! to src/main/resources/application.properties in my-app, the output will be "Message from MyService: Hello from Application!". If we don’t set the property, it will be "Message from MyService: Default Message".

The real power of starters isn’t just about providing beans; it’s about providing conditional and configurable beans. The @Conditional annotations are what allow starters to be intelligent. They don’t blindly enable configurations; they check the environment, existing beans, and properties. This makes starters flexible and prevents conflicts. The EnableConfigurationProperties annotation is the bridge between your starter’s auto-configuration and the application’s external properties, allowing for fine-grained control without requiring code changes in the consuming application.

The META-INF/spring.factories file is the discovery mechanism, but it’s also a potential point of failure if misconfigured. If you have multiple starters, you’ll see multiple entries separated by commas in that file.

The next challenge is managing multiple related auto-configurations within a single starter, often controlled by a single parent @Configuration class.

Want structured learning?

Take the full Spring-boot course →