“Using @Value is like grabbing tools from a messy bag, hoping you get the right one. @ConfigurationProperties is like having a perfectly organized toolbox where every tool has its labeled drawer. 🧰🔧”

As your Spring Boot application grows, so does its configuration. You start with a simple database URL, but soon you have API keys, timeout settings, feature flags, and custom messages. The common approach is to sprinkle @Value("${some.property}") annotations throughout your services. While this works, your configuration becomes scattered, string-typed, and fragile. A single typo in a property name can lead to runtime errors.

It’s time to trade that messy tool bag for a professional, organized toolbox. In this tutorial, we’ll explore @ConfigurationProperties, the modern, type-safe, and structured way to manage your application’s settings, making your code cleaner, safer, and a joy to work with.


🚩 Prerequisites

  1. Java 17+ and a Spring Boot 3+ project.
  2. Maven or Gradle.
  3. Basic understanding of Spring beans and dependency injection.
  4. You’ll need to add the validation starter if it’s not already present to use validation features:
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

1️⃣ The Messy Tool Bag: Limitations of @Value

Let’s imagine a service that connects to an external “Banana API.” Using @Value, it might look like this:

package com.example.blog.service;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class BananaApiClient {

    @Value("${banana.api.url}")
    private String apiUrl;

    @Value("${banana.api.key}")
    private String apiKey;

    @Value("${banana.api.timeout-ms:5000}") // Default value
    private int timeout;

    public void fetchBananas() {
        System.out.println("Connecting to " + apiUrl + " with key " + apiKey);
        System.out.println("Timeout is set to " + timeout + "ms");
        // ... logic to call the external API
    }
}

This works, but it has several drawbacks:

  • Scattered: These properties are logically related, but the annotations are scattered in the service class. If you need to add another property, you have to add another @Value annotation.
  • Not Type-Safe: Everything is based on strings. If someone puts “five-thousand” instead of “5000” in the properties file, the application will fail to start.
  • Error-Prone: A typo in the property name (e.g., "${banna.api.url}") won’t be caught at compile time and will cause a startup failure.
  • No IDE Support: You don’t get auto-completion for these properties in your .properties file without extra configuration.

2️⃣ Building Your Toolbox: Creating a @ConfigurationProperties Class

Let’s create a dedicated class to hold all our Banana API settings. This can be a standard POJO or, even better, an immutable Java Record.

First, let’s define the properties in src/main/resources/application.properties. Notice the structured, prefixed naming convention.

# Banana API Configuration
banana.api.url=https://api.jungle.com/bananas
banana.api.key=SECRET_JUNGLE_KEY
banana.api.timeout-ms=2000

Now, let’s create our toolbox class. The @ConfigurationProperties annotation tells Spring to bind all properties starting with the prefix "banana.api" to the fields of this record.

package com.example.blog.config;

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

// Using a Java Record for immutability and conciseness
@ConfigurationProperties(prefix = "banana.api")
public record BananaApiProperties(String url, String key, int timeoutMs) {
    // Spring Boot will automatically map:
    // banana.api.url -> url
    // banana.api.key -> key
    // banana.api.timeout-ms -> timeoutMs (kebab-case to camelCase mapping)
}

3️⃣ Enabling Your Properties Class

Just creating the class isn’t enough. We need to tell Spring to scan for it and register it as a bean. You have two primary ways to do this.

Method A: The Modern @ConfigurationPropertiesScan (Recommended)

This is the simplest approach. Just add the @ConfigurationPropertiesScan annotation to your main application class. Spring will then automatically scan for any @ConfigurationProperties beans in your project.

package com.example.blog;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

@SpringBootApplication
@ConfigurationPropertiesScan // Scans for and registers @ConfigurationProperties beans
public class BlogApplication {

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

Method B: The Explicit @EnableConfigurationProperties

Alternatively, you can explicitly register your properties classes on any @Configuration class. This gives you more granular control if you don’t want to scan the entire classpath.

package com.example.blog.config;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties(BananaApiProperties.class)
public class AppConfig {
    // This class enables the BananaApiProperties bean
}

4️⃣ Organizing the Drawers: Nested Objects and Lists

Real-world configuration is often more complex. Let’s create a more advanced properties class for our entire application, which includes the Banana API client settings as a nested object and a list of admins.

Here is our new application.yml. YAML is often easier for nested structures.

app:
  name: "Jungle Blog"
  admins: # A list of strings
    - "chief-monkey@jungle.com"
    - "admin-gorilla@jungle.com"
  client: # A nested object
    banana-api:
      url: https://api.jungle.com/bananas
      key: SECRET_JUNGLE_KEY
      timeout-ms: 3000

And here is the corresponding @ConfigurationProperties class that maps this structure.

package com.example.blog.config;

import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

import java.util.List;

@ConfigurationProperties(prefix = "app")
@Validated // Enable validation on this properties bean
public record AppProperties(
    @NotEmpty String name,
    @NotNull List<String> admins,
    @NotNull Client client
) {
    // Nested record for the client properties
    public record Client(
        @NotNull BananaApi bananaApi
    ) {}

    // Nested record for the banana-api properties
    public record BananaApi(
        @NotEmpty String url,
        @NotEmpty String key,
        int timeoutMs
    ) {}
}
  • Nesting: We created nested records (Client, BananaApi) that perfectly mirror the structure in our YAML file.
  • Lists: Spring automatically binds comma-separated values in .properties or YAML sequences into a List.
  • Validation: By adding @Validated to the class and JSR 303 annotations (like @NotEmpty) to the fields, Spring will validate the properties at startup. If app.name is missing, the application will fail fast with a clear error message!

5️⃣ Verification: Using Your Perfectly Organized Toolbox

Now, let’s refactor our BananaApiClient to use this new, clean, and type-safe configuration object. We can inject it just like any other Spring bean.

package com.example.blog.service;

import com.example.blog.config.AppProperties;
import org.springframework.stereotype.Service;

@Service
public class BananaApiClient {

    // Inject the entire properties object
    private final AppProperties.BananaApi bananaApiConfig;

    public BananaApiClient(AppProperties appProperties) {
        // Access the nested properties in a type-safe way
        this.bananaApiConfig = appProperties.client().bananaApi();
    }

    public void fetchBananas() {
        System.out.println("Connecting to " + bananaApiConfig.url() + " with key " + bananaApiConfig.key());
        System.out.println("Timeout is set to " + bananaApiConfig.timeoutMs() + "ms");
        // ... logic to call the external API
    }
}

To see it all in action, we can use a CommandLineRunner to print the properties at startup.

package com.example.blog;

import com.example.blog.config.AppProperties;
import com.example.blog.service.BananaApiClient;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
@ConfigurationPropertiesScan
public class BlogApplication {

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

    @Bean
    CommandLineRunner runner(AppProperties appProperties, BananaApiClient client) {
        return args -> {
            System.out.println("--- Configuration Properties Loaded ---");
            System.out.println("App Name: " + appProperties.name());
            System.out.println("Admins: " + appProperties.admins());
            System.out.println("Banana API URL: " + appProperties.client().bananaApi().url());
            System.out.println("-------------------------------------");

            // Use the client service
            client.fetchBananas();
        };
    }
}

💡 Monkey-Proof Tips

  • IDE Auto-Completion is Your Best Friend: One of the biggest wins of @ConfigurationProperties is IDE support. Most IDEs like IntelliJ will automatically provide auto-completion for your custom properties in .properties and .yml files. No more guessing or typos!
  • Use Records for Immutability: Using a Java record for your properties class is a great modern practice. It makes your configuration object immutable, which is safer in a multi-threaded environment.
  • Fail Fast with Validation: Always use the @Validated annotation on your properties class. It’s much better for your application to fail to start due to a missing property than to throw a NullPointerException at a random time in production.
  • Kebab-case to camelCase: Remember that Spring Boot can automatically bind kebab-case in your property files (e.g., timeout-ms) to camelCase field names in your Java class (e.g., timeoutMs). This is the recommended convention.

🚀 Challenge

You have a service that sends emails and is currently configured with multiple @Value annotations. Your mission is to refactor it to use a dedicated @ConfigurationProperties class.

Before (The Messy Way):

@Service
public class EmailService {
    @Value("${email.smtp.host}")
    private String host;
    @Value("${email.smtp.port}")
    private int port;
    @Value("${email.from-address}")
    private String fromAddress;
    @Value("${email.default-subject}")
    private String defaultSubject;
    // ...
}

Your task:

  1. Create a new record or class named EmailProperties.
  2. Annotate it with @ConfigurationProperties(prefix = "email").
  3. Create nested properties for the smtp settings (host and port).
  4. Move all the email-related properties from @Value into this new class.
  5. Add validation to ensure the host and fromAddress are not empty.
  6. Enable the properties bean using @ConfigurationPropertiesScan.
  7. Refactor the EmailService to inject and use the new EmailProperties bean.

👏 Congratulations! You’ve officially organized your configuration jungle. By mastering @ConfigurationProperties, you’re now writing code that is not only cleaner and more robust but also easier to maintain and understand. Your future self (and your teammates) will thank you!

By admin

Leave a Reply

Your email address will not be published. Required fields are marked *