Type-Safe Configuration with @ConfigurationProperties
Introduction
This tutorial explains@ConfigurationProperties, Spring Boot's way of binding a whole group of related settings to one typed Java object. We will start with the problems of scattering many @Value annotations through an application. We will then bind a group of shop settings - strings, numbers, a nested object, a list, a map, a duration and a data size - to a Java record, and inject it into a service. Next we will give properties default values, validate them at startup with Bean Validation, and see the error Spring Boot prints when a value is wrong. We will then look at the configuration metadata that gives IDEs auto-completion for your own properties, bind properties to a class from a library, and finish by comparing @ConfigurationProperties with @Value.(The configuration files and property sources used here are explained in application.properties vs application.ymlClick to read tutorial: application.properties vs application.yml in Spring Boot.)
The problem with many @Value annotations
In the tutorial on SpEL and @ValueClick to read tutorial: Spring Expression Language (SpEL) and @Value Explained, each setting was injected with its own@Value("${...}"). That is fine for one or two values, but for a real feature with ten settings it gets awkward -
- The property names are strings repeated wherever a setting is used, so a typo is only found at runtime.
- Settings that belong together - the shop's name, page size, currencies, contact details - are scattered across constructors and classes.
- There is no single place that documents which settings a feature has, their types and their defaults.
- Validating the values - "the page size must be between 1 and 100" - has to be done by hand.
@ConfigurationProperties solves all four by mapping a property prefix, such as shop, to a class whose fields are the settings.
Binding settings to a record
The configuration file holds the shop settings, including a duration and a data size written in Spring Boot's friendly formats -shop.properties - the settings to bind
shop.name=JavaBrahman Books
shop.currencies=USD,EUR,INR
shop.contact.email=orders@example.com
shop.contact.phone=+44 20 7946 0000
shop.delivery-days.GB=2
shop.delivery-days.IN=5
shop.session-timeout=30m
shop.max-upload-size=10MB
@ConfigurationProperties("shop"). Each component's name matches a property after the prefix, and nested settings become a nested record -
ShopProperties.java - the shop settings as a record
//ShopProperties.java
package com.javabrahman.boot.props;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.util.unit.DataSize;
/**
* Settings of the online shop.
*
* @param name name of the shop, shown on every page
* @param pageSize number of books listed per page
* @param currencies currencies the shop accepts
* @param contact contact details shown on the help page
* @param deliveryDays expected delivery time in days, per country code
* @param sessionTimeout how long an idle shopping session is kept
* @param maxUploadSize largest file a customer may upload
*/
@ConfigurationProperties("shop")
public record ShopProperties(
String name,
@DefaultValue("20") int pageSize,
List<String> currencies,
Contact contact,
Map<String, Integer> deliveryDays,
Duration sessionTimeout,
DataSize maxUploadSize) {
/**
* Contact details.
*
* @param email address for orders
* @param phone phone number for orders
*/
public record Contact(String email, String phone) {
}
}
@ConfigurationProperties classes -
Injecting ShopProperties into a service
//ShopPropertiesApp.java
package com.javabrahman.boot.props;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.stereotype.Service;
@Service
class StorefrontService {
private final ShopProperties shop;
StorefrontService(ShopProperties shop) {
this.shop = shop;
}
String banner() {
return shop.name() + " - " + shop.pageSize() + " books per page, prices in " + String.join("/", shop.currencies())
+ ", delivery to India in " + shop.deliveryDays().get("IN") + " days";
}
}
@SpringBootApplication
@ConfigurationPropertiesScan // finds @ConfigurationProperties classes in this package and below
public class ShopPropertiesApp {
public static void main(String[] args) {
try (var context = SpringApplication.run(ShopPropertiesApp.class, args)) {
ShopProperties shop = context.getBean(ShopProperties.class);
System.out.println(context.getBean(StorefrontService.class).banner());
System.out.println("contact = " + shop.contact());
System.out.println("sessionTimeout = " + shop.sessionTimeout() + " (" + shop.sessionTimeout().toMinutes() + " minutes)");
System.out.println("maxUploadSize = " + shop.maxUploadSize() + " (" + shop.maxUploadSize().toKilobytes() + " KB)");
}
}
}
JavaBrahman Books - 20 books per page, prices in USD/EUR/INR, delivery to India in 5 days contact = Contact[email=orders@example.com, phone=+44 20 7946 0000] sessionTimeout = PT30M (30 minutes) maxUploadSize = 10485760B (10240 KB)
Explanation of the code
@ConfigurationProperties("shop")binds every property undershop.to the record. Relaxed binding mapspage-sizetopageSizeanddelivery-daystodeliveryDays.- Because a record has a single constructor, Spring Boot uses constructor binding: it converts each value and calls the constructor. The result is immutable - nothing can change the settings after startup.
shop.page-sizeis not in the file, so@DefaultValue("20")supplied it.- Spring Boot converted the text to rich types:
USD,EUR,INRto aList, thedelivery-days.*keys to aMap,30mto aDurationof 30 minutes, and10MBto aDataSizeof 10 megabytes (10240 KB - Spring uses binary units). Durations also acceptms,s,handd, and ISO-8601 text such asPT30M. @ConfigurationPropertiesScanregisters every@ConfigurationPropertiesclass in its package and below as a bean. The alternative is to list classes explicitly with@EnableConfigurationProperties(ShopProperties.class).StorefrontServicedepends on one typed object instead of many strings. In a unit test, it can be given anew ShopProperties(...)directly.
Validating settings at startup
Wrong settings are best caught when the application starts, not when a customer hits the broken feature. Annotating the properties class with@Validated and its components with Bean Validation constraints makes Spring Boot validate them during binding. It needs the spring-boot-starter-validation dependency, which brings Hibernate Validator -
MailProperties.java - settings with validation constraints
//MailProperties.java
package com.javabrahman.boot.validated;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
@Validated
@ConfigurationProperties("mail")
public record MailProperties(
@NotBlank String host,
@Min(1) @Max(65535) int port,
@Email String from) {
}
An application that binds MailProperties
//MailApp.java
package com.javabrahman.boot.validated;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@SpringBootApplication
@EnableConfigurationProperties(MailProperties.class)
public class MailApp {
public static void main(String[] args) {
try (var context = SpringApplication.run(MailApp.class, args)) {
System.out.println("Started with " + context.getBean(MailProperties.class));
} catch (Exception e) {
System.out.println("Startup failed: " + e.getClass().getSimpleName());
}
}
}
Started with MailProperties[host=smtp.example.com, port=587, from=shop@example.com]
2026-11-06T13:45:44.725+05:30 WARN 29516 --- [ main] s.c.a.AnnotationConfigApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.boot.context.properties.ConfigurationPropertiesBindException: Error creating bean with name 'mail-com.javabrahman.boot.validated.MailProperties': Could not bind properties to 'MailProperties' : prefix=mail, ignoreInvalidFields=false, ignoreUnknownFields=true
2026-11-06T13:45:44.754+05:30 INFO 29516 --- [ main] .s.b.a.l.ConditionEvaluationReportLogger :
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
2026-11-06T13:45:44.801+05:30 ERROR 29516 --- [ main] o.s.b.d.LoggingFailureAnalysisReporter :
***************************
APPLICATION FAILED TO START
***************************
Description:
Binding to target com.javabrahman.boot.validated.MailProperties failed:
Property: mail.port
Value: "70000"
Origin: "mail.port" from property source "commandLineArgs"
Reason: must be less than or equal to 65535
Property: mail.from
Value: "shop-at-example.com"
Origin: "mail.from" from property source "commandLineArgs"
Reason: must be a well-formed email address
Action:
Update your application's configuration
Startup failed: ConfigurationPropertiesBindException
Explanation of the output
- With valid values the application started, and the record printed its components.
- With invalid values, binding failed and the application did not start. Spring Boot's failure analyzer printed an APPLICATION FAILED TO START report that names each invalid property, the value given, where the value came from (here, the command line), and the constraint it broke - followed by an Action line saying what to do.
- Before the report, Spring logged a
WARNline saying that the context refresh was cancelled, and a hint about the condition evaluation report. The report itself is logged atERRORlevel, which is why these lines carry timestamps. The example's own catch block then printed the final line. - Constraints on a nested record are checked too if the nested component is annotated with
@Valid.
Configuration metadata and IDE auto-completion
When you typeserver. in an application.properties file, IntelliJ IDEA, VS Code and Eclipse suggest Spring Boot's properties with their types, defaults and descriptions. They read this from configuration metadata: a JSON file at META-INF/spring-configuration-metadata.json inside each jar. Your own @ConfigurationProperties classes get the same support from spring-boot-configuration-processor, an annotation processor that writes the file while the code compiles. Choosing Spring Configuration Processor on Spring Initializr adds it to the Maven compiler plugin's annotation processor path -
pom.xml - the configuration processor, as Spring Initializr configures it
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>default-compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</execution>
</executions>
</plugin>
javac no longer runs annotation processors just because they are on the classpath, so a processor added only as an ordinary dependency would silently do nothing. (With plain javac, -proc:full switches classpath processors back on, which is how the examples in this tutorial were compiled.) The compiler wrote this file next to the classes -
OUTPUT - the generated META-INF/spring-configuration-metadata.json{
"groups": [
{
"name": "mail",
"type": "com.javabrahman.boot.validated.MailProperties",
"sourceType": "com.javabrahman.boot.validated.MailProperties"
},
{
"name": "shop",
"type": "com.javabrahman.boot.props.ShopProperties",
"sourceType": "com.javabrahman.boot.props.ShopProperties"
},
{
"name": "shop.contact",
"type": "com.javabrahman.boot.props.ShopProperties$Contact",
"sourceType": "com.javabrahman.boot.props.ShopProperties",
"sourceMethod": "contact()"
}
],
"properties": [
{
"name": "mail.from",
"type": "java.lang.String",
"sourceType": "com.javabrahman.boot.validated.MailProperties"
},
{
"name": "mail.host",
"type": "java.lang.String",
"sourceType": "com.javabrahman.boot.validated.MailProperties"
},
{
"name": "mail.port",
"type": "java.lang.Integer",
"sourceType": "com.javabrahman.boot.validated.MailProperties",
"defaultValue": 0
},
{
"name": "shop.contact.email",
"type": "java.lang.String",
"description": "address for orders",
"sourceType": "com.javabrahman.boot.props.ShopProperties$Contact"
},
{
"name": "shop.contact.phone",
"type": "java.lang.String",
"description": "phone number for orders",
"sourceType": "com.javabrahman.boot.props.ShopProperties$Contact"
},
{
"name": "shop.currencies",
"type": "java.util.List<java.lang.String>",
"description": "currencies the shop accepts",
"sourceType": "com.javabrahman.boot.props.ShopProperties"
},
{
"name": "shop.delivery-days",
"type": "java.util.Map<java.lang.String,java.lang.Integer>",
"description": "expected delivery time in days, per country code",
"sourceType": "com.javabrahman.boot.props.ShopProperties"
},
{
"name": "shop.max-upload-size",
"type": "org.springframework.util.unit.DataSize",
"description": "largest file a customer may upload",
"sourceType": "com.javabrahman.boot.props.ShopProperties"
},
{
"name": "shop.name",
"type": "java.lang.String",
"description": "name of the shop, shown on every page",
"sourceType": "com.javabrahman.boot.props.ShopProperties"
},
{
"name": "shop.page-size",
"type": "java.lang.Integer",
"description": "number of books listed per page",
"sourceType": "com.javabrahman.boot.props.ShopProperties",
"defaultValue": 20
},
{
"name": "shop.session-timeout",
"type": "java.time.Duration",
"description": "how long an idle shopping session is kept",
"sourceType": "com.javabrahman.boot.props.ShopProperties"
}
],
"hints": [],
"ignored": {
"properties": []
}
}
Explanation of the output
- The
groupssection lists each properties class and its prefix -mail,shop, andshop.contactfor the nested record. - The
propertiessection lists every property with its kebab-casename, its Javatype, and adescriptiontaken from the@paramJavadoc of the record. ThedefaultValueofshop.page-sizecame from@DefaultValue("20");mail.portshows0because a primitiveintdefaults to zero.MailPropertieshas no Javadoc, so its properties have no descriptions - a reason to document properties classes. - An IDE that finds this file in your project offers auto-completion and shows the descriptions as documentation, and it warns about unknown property names - which catches typos before the application even runs.
Binding properties to a library class
@ConfigurationProperties can also go on a @Bean method. Spring Boot then binds the prefix to the object the method returns, by calling its setters - a way to configure a class from a library that has no Spring annotations of its own. Spring Boot's own spring.datasource.* properties work this way for the connection pool -
Binding a prefix to a third-party object returned by a @Bean method
@Bean
@ConfigurationProperties("reports.http") // reports.http.connect-timeout=2s calls setConnectTimeout(...)
ReportClientSettings reportClientSettings() {
return new ReportClientSettings(); // a JavaBean from a library, with getters and setters
}
@ConfigurationProperties vs @Value
- Grouping and type safety:
@ConfigurationPropertiesbinds a whole prefix to one typed object;@Valueinjects one value at a time by a string key. - Relaxed binding: fully supported by
@ConfigurationProperties(page-size,pageSize,SHOP_PAGESIZE);@Valueshould always use the canonical kebab-case name. - Validation: built in with
@Validated; with@Value, it is manual. - Metadata and IDE support: generated for
@ConfigurationProperties; not for@Value. - SpEL expressions: supported only by
@Value(#{...}). - When to use which: use
@ConfigurationPropertiesfor any group of related settings - which is most configuration in a real application - and keep@Valuefor a single, isolated value or when you need an expression.
Summary
In this tutorial we first saw the problems of using many@Value annotations. We then bound a group of shop settings to an immutable record with @ConfigurationProperties, with a nested record, a list, a map, a Duration written as 30m, a DataSize written as 10MB and a default value, and injected it into a service. Next we validated settings with @Validated and saw the startup failure report for invalid values, which even names where each value came from. We then generated configuration metadata with the configuration processor for IDE auto-completion, bound properties to a library object through a @Bean method, and lastly compared @ConfigurationProperties with @Value. The next tutorial gives the same application different settings per environment: Spring Boot profilesClick to read tutorial: Spring Boot Profiles and Environment-Specific Configuration.Spring Boot Tutorials
application.properties vs application.yml in Spring BootClick to read tutorial: application.properties vs application.yml in Spring Boot Spring Boot Profiles and Environment-Specific ConfigurationClick to read tutorial: Spring Boot Profiles and Environment-Specific Configuration Your First Spring Boot Application with Spring InitializrClick to read tutorial: Your First Spring Boot Application with Spring Initializr How Spring Boot Auto-Configuration WorksClick to read tutorial: How Spring Boot Auto-Configuration Works Spring Boot Starters Explained with ExamplesClick to read tutorial: Spring Boot Starters Explained with Examples Logging in Spring Boot: SLF4J, Logback and Log LevelsClick to read tutorial: Logging in Spring Boot: SLF4J, Logback and Log Levels Spring Boot DevTools: Automatic Restart and ProductivityClick to read tutorial: Spring Boot DevTools: Automatic Restart and Productivity
application.properties vs application.yml in Spring BootClick to read tutorial: application.properties vs application.yml in Spring Boot Spring Boot Profiles and Environment-Specific ConfigurationClick to read tutorial: Spring Boot Profiles and Environment-Specific Configuration Your First Spring Boot Application with Spring InitializrClick to read tutorial: Your First Spring Boot Application with Spring Initializr How Spring Boot Auto-Configuration WorksClick to read tutorial: How Spring Boot Auto-Configuration Works Spring Boot Starters Explained with ExamplesClick to read tutorial: Spring Boot Starters Explained with Examples Logging in Spring Boot: SLF4J, Logback and Log LevelsClick to read tutorial: Logging in Spring Boot: SLF4J, Logback and Log Levels Spring Boot DevTools: Automatic Restart and ProductivityClick to read tutorial: Spring Boot DevTools: Automatic Restart and Productivity