“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
- Java 17+ and a Spring Boot 3+ project.
- Maven or Gradle.
- Basic understanding of Spring beans and dependency injection.
- 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 aList
. - Validation: By adding
@Validated
to the class and JSR 303 annotations (like@NotEmpty
) to the fields, Spring will validate the properties at startup. Ifapp.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 aNullPointerException
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:
- Create a new record or class named
EmailProperties
. - Annotate it with
@ConfigurationProperties(prefix = "email")
. - Create nested properties for the
smtp
settings (host
andport
). - Move all the email-related properties from
@Value
into this new class. - Add validation to ensure the
host
andfromAddress
are not empty. - Enable the properties bean using
@ConfigurationPropertiesScan
. - Refactor the
EmailService
to inject and use the newEmailProperties
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!