Demystifying Spring Boot Magic: Starters, Auto-configuration, and Building Your Own Library

Prashant Bhardwaj
11 min readDec 27, 2024

--

Two of its most powerful features of Spring Boot are Starters and Auto-configuration, which have revolutionized Java development by simplifying the process of creating production-ready applications.. These features work behind the scenes, significantly reducing the boilerplate code you need to write. But have you ever wondered how they work?

In this blog, we’ll demystify Spring Boot starters and auto-configuration. We’ll then take it a step further and guide you through building your own Java library, complete with auto-configuration and a custom starter, to experience the magic firsthand.

Part 1: Understanding Spring Boot Starters

Think of Spring Boot Starters as convenient dependency aggregators. Instead of manually adding multiple dependencies to your project, you can simply include a single starter, and it will pull in all the related dependencies you need for a specific type of functionality.

Example:

Let’s say you want to build a web application. Instead of adding dependencies for Spring MVC, Tomcat, Jackson, etc., separately, you can simply add the spring-boot-starter-web dependency:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

This single starter will bring in all the necessary dependencies for building a web app.

Some other commonly used starters include:

  • spring-boot-starter-data-jpa: For working with databases using Spring Data JPA.
  • spring-boot-starter-security: For securing your application with Spring Security.
  • spring-boot-starter-test: For testing your application (includes JUnit, Mockito, etc.).

Key benefits of using starters:

  • Reduced Dependency Management: You don’t need to hunt down and configure individual libraries in maven (or gradle).
  • Consistent Versions: Starters ensure that you’re using compatible versions of dependencies, (unless in your pom, you specify the dependencies which are coming from starter pom and provide a version with that).
  • Simplified pom.xml or build.gradle: Your build files become cleaner and more readable.

Code of all the starters can be seen here — https://github.com/spring-projects/spring-boot/tree/main/spring-boot-project/spring-boot-starters

Note: As we mentioned in the beginning of this blog that you can create your own starters, similarly some people have created other open source starters which are not part of Springboot. List of such starters is given on the link given above. Your starter name should not start with Springboot, as a general guideline.

Part 2: Understanding Spring Boot Auto-configuration

Auto-configuration is the real magic behind Spring Boot’s ease of use. It automatically configures your Spring application based on the dependencies present in your classpath. Based on the presence of some specific classes on the class path, Spring Boot intelligently guesses what you need and sets it up for you. And Springboot starters also have a role here as they bring those specific libraries and classes on the classpath.

Example:

Let’s say you want to create an application using Springboot, which connects to Cassandra database.

  1. The starting point: Dependencies Introduced by spring-boot-starter-data-cassandra

When you include spring-boot-starter-data-cassandra in your project, it transitively pulls in several key dependencies:

  • spring-boot-starter: Core starter, including auto-configuration support, logging, and YAML.
  • spring-data-cassandra: The core library that provides Spring Data abstractions for Cassandra. This includes:
    - CassandraTemplate for interacting with Cassandra.
    - Repository support.
    - Mapping annotations.
  • com.datastax.oss:java-driver-core: The DataStax Java driver, responsible for the low-level communication with your Cassandra cluster.
  • com.datastax.oss:java-driver-mapper-runtime: Provides an annotation-based mapper (object-graph mapping) to interact with Cassandra.

2. Spring Boot Auto configuration: mechanism explanation

Spring Boot’s auto-configuration mechanism kicks in based on the presence of these dependencies.

Here’s how it works:

  • @EnableAutoConfiguration and spring.factories (The Old Way): In older Spring Boot versions, auto-configuration was enabled using the @EnableAutoConfiguration annotation. This annotation, usually part of the @SpringBootApplication annotation, instructed Spring Boot to look for a special file named spring.factories located in the META-INF directory of jars on the classpath. The spring.factories file contained a list of auto-configuration classes under the key org.springframework.boot.autoconfigure.EnableAutoConfiguration.
  • org.springframework.boot.autoconfigure.AutoConfiguration.imports (The New Way): Starting from Spring Boot 2.7, a new mechanism was introduced. Auto-configuration classes are now listed in a file named org.springframework.boot.autoconfigure.AutoConfiguration.imports located in the META-INF/spring directory [code is here]. This makes the auto-configuration process more explicit and discoverable. Each line in this file specifies the fully qualified name of an auto-configuration class. If you are building a library that you intend to distribute and reuse across multiple projects, then create the org.springframework.boot.autoconfigure.AutoConfiguration.imports file and put that under META-INF/spring directory. Add the fully qualified name of your auto-configuration class on a new line in this file. com.example.myautoconfig.MyAutoConfiguration. When a user includes your library as a dependency in their Spring Boot application, Spring Boot will discover your org.springframework.boot.autoconfigure.AutoConfiguration.imports file. It will then process your auto-configuration class just like it processes its own, checking conditions and creating beans as defined.
  • The spring-boot-autoconfigure Module: The heart of the auto-configuration mechanism lives in the spring-boot-project/spring-boot-autoconfigure module [code is here]. This module contains the org.springframework.boot.autoconfigure.AutoConfiguration.imports file, which acts as a central registry of all auto-configuration classes provided by Spring Boot itself. Springboot’s own auto-configure packages like for web or Cassandra reside inside this module. As you see that all of those auto-configure packagesauto-configure packages automatically come with Springboot.
  • You can also use @Import(MyAutoConfiguration.class) which will be used by Springboot to load the beans written in MyAutoConfiguration.class.
  • Classpath Scanning and Conditional Configuration: When your Spring Boot application starts, it scans the classpath. For each auto-configuration class listed in org.springframework.boot.autoconfigure.AutoConfiguration.imports, Spring Boot checks if the conditions specified by that class are met. This is where conditional annotations come into play.

3. The @AutoConfiguration annotation

Introduced in Spring Boot 2.7 as a replacement for listing auto-configurations in spring.factories under the EnableAutoConfiguration key, the @AutoConfiguration annotation serves the following purposes:

  1. Marker Annotation: It acts as a marker to indicate that a class is an auto-configuration class. While not strictly mandatory for the auto-configuration process to work (as the class would still be picked up from the imports file), it provides a clear and explicit way to identify auto-configuration classes within your codebase.
  2. Metadata for Ordering: @AutoConfiguration can be used in conjunction with @AutoConfigureBefore and @AutoConfigureAfter to specify the order in which auto-configuration classes should be applied. This is important for ensuring that dependencies between auto-configurations are resolved correctly. For example, if auto-configuration class A depends on a bean created by auto-configuration class B, then B should be configured before A.

How @AutoConfiguration Relates to org.springframework.boot.autoconfigure.AutoConfiguration.imports

  • The imports File is the Source of Truth: The org.springframework.boot.autoconfigure.AutoConfiguration.imports file is the primary mechanism for Spring Boot to discover auto-configuration classes. Any class listed in this file will be considered for auto-configuration, regardless of whether it's annotated with @AutoConfiguration or not.
  • @AutoConfiguration is for Clarity and Ordering: The @AutoConfiguration annotation adds clarity and structure to your codebase. It's a best practice to annotate your auto-configuration classes with it. Furthermore, when you need to define a specific order of execution between auto-configuration classes @AutoConfiguration becomes important.

Example Usage:

@AutoConfiguration
@ConditionalOnClass(CassandraOperations.class)
@EnableConfigurationProperties(CassandraProperties.class)
@Import({ CassandraDataConfiguration.class, ThrottlingCassandraAutoConfiguration.class })
public class CassandraDataAutoConfiguration {
//...
}

Key Differences from the Old spring.factories Approach

  1. Explicit vs. Implicit: The org.springframework.boot.autoconfigure.AutoConfiguration.imports approach makes auto-configuration more explicit. You can see all the auto-configuration classes that are in play by looking at this file. With spring.factories, it was less obvious which auto-configurations were being applied.
  2. Discoverability: The new approach improves discoverability. It’s easier to find and understand the available auto-configurations.
  3. Ordering: While ordering could be controlled using @AutoConfigureBefore and @AutoConfigureAfter with the old approach as well, using these annotations along with @AutoConfiguration provides a more dedicated and standardized mechanism for ordering.

4. Auto-configuration Classes: The Building Blocks

All the auto configuration classes use @AutoConfiguration annotation (Code is here). And @AutoConfiguration annotation class uses @Configuration annotation. Which makes an auto-configuration class a regular Java class annotated with @Configuration. This signals to Spring that it should be processed as a source of bean definitions. However, what makes it an "auto-configuration" class are the special conditional annotations that control its behavior.

The primary auto-configuration classes involved here are:

org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration:

This class is the heart of Cassandra auto-configuration. It is responsible for setting up the core beans needed to work with Cassandra in a Spring Boot application. It imports other auto-configuration classes for specific functionalities. [Code is here]

- @Configuration: Marks it as a configuration class.
- @ConditionalOnClass({ CqlSession.class, CassandraTemplate.class }): This is crucial. It tells Spring Boot to only process this class if both CqlSession (from the DataStax driver) and CassandraTemplate (from Spring Data Cassandra) are present on the classpath. This condition is met because spring-boot-starter-data-cassandra brings in these dependencies.
- @EnableConfigurationProperties(CassandraProperties.class): This annotation enables the use of configuration properties defined in the CassandraProperties class. We'll discuss properties in detail later.
- @Import(...): This annotation is used to import other configuration classes like CassandraDataConfiguration which has the default configuration for Cassandra and ThrottlingCassandraAutoConfiguration which provides default configuration related to request throttling.

org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration:

This class is responsible for configuring a CqlSession using the DataStax Java driver. [Code is here]

org.springframework.boot.autoconfigure.data.cassandra.CassandraRepositoriesAutoConfiguration:

Enables auto-configuration of Spring Data Cassandra Repositories. [Code is here]

org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveDataAutoConfiguration:

This class is responsible for setting up the core beans needed to work with Cassandra in a Spring Boot Reactive application. [Code is here]

org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveRepositoriesAutoConfiguration:

Enables auto-configuration of Spring Data Cassandra Reactive Repositories. [Code is here]

5. Conditional Annotations: The Gatekeepers

Conditional annotations are the heart of auto-configuration. They determine whether a particular auto-configuration class or a specific bean definition within that class should be applied. Here are some of the most important ones, with examples from Cassandra’s auto-configuration:

@ConditionalOnClass:

  • Example: @ConditionalOnClass({ CqlSession.class, CassandraTemplate.class }) (on CassandraDataAutoConfiguration)
  • Purpose: Ensures that the configuration is applied only if the specified classes are present on the classpath.

@ConditionalOnMissingBean:

  • Example: @ConditionalOnMissingBean(CassandraTemplate.class) (on the cassandraTemplate bean method within CassandraDataAutoConfiguration)
  • Purpose: Creates a bean only if no other bean of the same type is already defined in the application context. This allows you to override Spring Boot’s default beans.

@ConditionalOnProperty:

  • Example: @ConditionalOnProperty(prefix = "spring.data.cassandra", name = "local-datacenter") (on various bean methods in CassandraAutoConfiguration)
  • Purpose: Applies the configuration only if a specific property is set (or not set) in your application.properties or application.yml.

@ConditionalOnSingleCandidate:

  • Example: @ConditionalOnSingleCandidate(CassandraConverter.class) (on the cassandraTemplate bean method within CassandraDataAutoConfiguration)
  • Purpose: Creates a bean only if there is exactly one bean of the specified type in the application context.

@ConditionalOnBean:

  • Example: @ConditionalOnBean(CqlSession.class) (on the cassandraSession bean method within CassandraDataConfiguration)
  • Purpose: Creates a bean only if a bean of the specified type is already defined in the application context.

@AutoConfigureAfter:

  • Example: @AutoConfigureAfter(CassandraAutoConfiguration.class) (on CassandraDataAutoConfiguration)
  • Purpose: Specifies that this auto-configuration should be applied after another auto-configuration class. This helps to control the order of configuration.

@AutoConfigureBefore:

  • Example: @AutoConfigureBefore(CassandraReactiveDataAutoConfiguration.class) (on CassandraDataAutoConfiguration)
  • Purpose: Specifies that this auto-configuration should be applied before another auto-configuration class.

6. Configuration Properties: Externalized Settings

Auto-configuration often relies on externalized configuration properties to customize its behavior. For example the url and port of Cassandra database server. These properties are typically defined in your application.properties or application.yml file.

  • @ConfigurationProperties: This annotation is used to bind properties to a Java class. For Cassandra, Spring Boot provides the org.springframework.boot.autoconfigure.cassandra.CassandraProperties class. [Code is here]
  • @EnableConfigurationProperties: This annotation, used in auto-configuration classes, enables the processing of @ConfigurationProperties classes. [Look here]

Example: CassandraProperties

The CassandraProperties class is annotated with @ConfigurationProperties(prefix = "spring.data.cassandra"). This means that any property in your application.properties that starts with spring.data.cassandra will be mapped to a field in this class.

@ConfigurationProperties(prefix = "spring.data.cassandra")
public class CassandraProperties {

private String keyspaceName;
private String contactPoints = "localhost"; // Default value
private int port = 9042; // Default value
private String localDatacenter;

// ... getters and setters ...
}

In your application.properties, you can then set:

spring.data.cassandra.keyspace-name=mykeyspace
spring.data.cassandra.contact-points=cassandra1.example.com,cassandra2.example.com
spring.data.cassandra.local-datacenter=datacenter1

7. How Auto-Configuration Uses Properties

Auto-configuration classes inject the CassandraProperties bean and use its values to configure other beans.

Example:

In CassandraAutoConfiguration, the CqlSession bean is configured using values from CassandraProperties:

    @Bean
@ConditionalOnMissingBean
public CqlSession cqlSession(CassandraProperties properties) {

CqlSessionBuilder builder = CqlSession.builder();

// ... many other configurations based on properties ...

if (StringUtils.hasText(properties.getLocalDatacenter())) {
builder.withLocalDatacenter(properties.getLocalDatacenter());
}
return builder.build();
}

8. Putting It All Together: The Cassandra Example

  1. You add spring-boot-starter-data-cassandra.
  2. Spring Boot finds org.springframework.boot.autoconfigure.AutoConfiguration.imports in spring-boot-autoconfigure.jar.
  3. It sees CassandraDataAutoConfiguration listed there.
  4. @ConditionalOnClass on CassandraDataAutoConfiguration passes because the starter brought in the necessary dependencies.
  5. @EnableConfigurationProperties(CassandraProperties.class) is processed, making CassandraProperties available.
  6. Properties from your application.properties (or defaults) are bound to CassandraProperties.
  7. CassandraAutoConfiguration creates a CqlSession bean, potentially using values from CassandraProperties.
  8. CassandraDataAutoConfiguration creates CassandraTemplate, CassandraConverter, etc., beans, also potentially using values from CassandraProperties and the CqlSession bean. The @ConditionalOnMissingBean checks allow you to override these beans if needed.
  9. Other auto-configuration classes (like CassandraRepositoriesAutoConfiguration if you use repositories) are processed similarly.

Part 3: Building Your Own Auto-configurable Library

Now, let’s get our hands dirty and build our own library that can be auto-configured by Spring Boot.

Scenario:

Let’s create a simple library that provides a “Greeting Service.” This service will have a method to generate a personalized greeting message.

Project Structure:

We will create 2 projects:

  1. my-greeting-library: Our library that contains the GreetingService
  2. my-greeting-starter: Our starter that handles the autoconfiguration of the beans in our library.

Step 1: Create the my-greeting-library project

  • Create a new Maven or Gradle project.
  • Add the following dependency in pom.xml (or build.gradle):
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>3.1.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>3.1.3</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>my-greeting-library</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
  • Create GreetingAutoConfiguration class:
// GreetingAutoConfiguration.java
package com.example.starter;

import com.example.mylibrary.DefaultGreetingService;
import com.example.mylibrary.GreetingProperties;
import com.example.mylibrary.GreetingService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConditionalOnClass(GreetingService.class)
@EnableConfigurationProperties(GreetingProperties.class)
public class GreetingAutoConfiguration {

@Bean
@ConditionalOnMissingBean
public GreetingService greetingService(GreetingProperties greetingProperties) {
return new DefaultGreetingService(greetingProperties);
}
}

Explanation:

  • @Configuration: Marks this class as a source of bean definitions.
  • @ConditionalOnClass(GreetingService.class): This configuration will only be applied if the GreetingService class is present on the classpath (meaning our library is included).
  • @EnableConfigurationProperties(GreetingProperties.class): Enables binding of properties from application.properties or application.yml to the GreetingProperties bean, allowing external configuration.
  • @Bean: Defines the greetingService bean.
  • @ConditionalOnMissingBean: This bean will only be created if there isn't already a bean of type GreetingService defined in the application context. This allows users to override our default implementation if they want.

Step 3: Create spring.factories file

  • In your my-greeting-starter project, create a file named spring.factories under src/main/resources/META-INF/.
  • Add the following content to spring.factories:
org.springframework.boot.autoconfigure.AutoConfiguration.imports = 
com.example.starter.GreetingAutoConfiguration

Step 4: Create the spring-autoconfigure-metadata.properties file

  • In your my-greeting-starter project, create a file named spring-autoconfigure-metadata.properties under src/main/resources/META-INF/.
  • Add the following content to spring-autoconfigure-metadata.properties:
# Auto Configure
com.example.starter.GreetingAutoConfiguration.ConditionalOnClass =
com.example.mylibrary.GreetingService

Step 5: Build the starter and install both packages to your local repository

  • Build the starter. You should have the starter jar in your target directory.
  • Use mvn install (or equivalent gradle command) to install both my-greeting-library and my-greeting-starter to your local Maven repository.

Part 4: Using Your Custom Starter

Now, let’s create a new Spring Boot application and use our custom starter.

Step 1: Create a new Spring Boot project

  • Use Spring Initializr or your IDE to create a new project.
  • Add our custom starter as a dependency:
<dependency>
<groupId>com.example</groupId>
<artifactId>my-greeting-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

Step 2: Create a REST Controller (optional)

Let’s create a simple REST controller to test our greeting service:

// GreetingController.java
package com.example.demo;

import com.example.mylibrary.GreetingService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GreetingController {

private final GreetingService greetingService;

public GreetingController(GreetingService greetingService) {
this.greetingService = greetingService;
}

@GetMapping("/greet")
public String greet(@RequestParam(value = "name", defaultValue = "World") String name) {
return greetingService.greet(name);
}
}

Step 3: Configure application.properties (optional)

You can customize the greeting message in application.properties or application.yml:

greeting.message=Howdy, %s!

Step 4: Run the Application

Run your Spring Boot application. If you access http://localhost:8080/greet?name=John, you should see the output: Howdy, John! (or Hello, John! if you didn't customize the message).

Conclusion:

Congratulations! You’ve successfully created your own auto-configurable library and custom starter in Spring Boot. You’ve learned how Spring Boot starters simplify dependency management, how auto-configuration magically configures your application, and how to build your own reusable components that integrate seamlessly with the Spring Boot ecosystem.

This knowledge opens up a whole new level of possibilities for building modular, maintainable, and easily configurable Spring Boot applications. You can now create libraries that can be effortlessly reused across multiple projects, reducing development time and promoting consistency. Keep exploring, keep building, and enjoy the power of Spring Boot!

--

--

Prashant Bhardwaj
Prashant Bhardwaj

Written by Prashant Bhardwaj

Masters in Business Administration and in Computer Application 🌐 https://prashantbhardwaj.com

No responses yet