Configuration · Lesson 24 of 95
@ConfigurationProperties
Master @ConfigurationProperties in Spring Boot: bind grouped settings to a record, use nested values, lists, Duration and validation with a clinic example.
A clinic keeps a laminated sheet at the reception: clinic name, number of tokens per day, length of each slot, phone number and the departments that are open. Every fact is on one sheet, under one title. Nobody asks the receptionist for the phone number, then the slot length, then the tokens, one question at a time.
@ConfigurationProperties gives your Spring Boot app the same sheet. You describe the settings once in a small class, and Spring fills the whole class from your configuration file.
What is @ConfigurationProperties?
Picture a printed form with boxes: name, tokens, slot length. You hand the form to Spring together with the settings file, and Spring writes every value into the matching box. If a value does not fit its box, for example text where a number belongs, Spring refuses to start and tells you which box is wrong.
The class or record becomes a normal bean, so you can inject it anywhere, exactly like a service.
Why is it used?
- Related settings stay together. The whole clinic setup lives in one type instead of many loose
@Valuelines. - Type safety. Numbers, lists, nested groups and durations such as
15mare converted for you. - Validation at startup. A wrong setting is caught when the app starts, not on the day a patient books a slot.
- Relaxed binding. The key
max-daily-tokensin the file binds to the fieldmaxDailyTokensin Java. - Easy to test. You can create the properties object by hand in a test.
How it works
Spring Boot reads the Environment, finds every key that starts with your prefix, matches key names to fields, converts the values, runs validation, and only then hands the finished object to the beans that need it.
textapplication.yml clinic.name clinic.max-daily-tokens | v bind by prefix +---------------------+ | ClinicProperties | | name, maxDailyTokens| +---------------------+ | v validate ok? ----> inject into beans no? ----> stop at startup
The prefix in the annotation decides which keys belong to this class. A nested record such as Contact binds to the keys under clinic.contact, and a List binds to a YAML list. If validation fails, the application stops with a clear message and never serves a single request with bad settings.
Real-Life Example
Sunrise Clinic runs a token system for its patients. Each doctor slot lasts 15 minutes, the clinic gives out at most 60 tokens a day, and it has three departments. The receptionist also needs the phone number printed on every slip. Keeping these as separate @Value fields would spread them across many classes. One properties record keeps them all in one place, and if someone types 500 tokens by mistake, the app refuses to start.
Code Example
Let's build the Sunrise Clinic service. It binds a nested group and a list, uses a default for the slot length, and validates the token limit.
File: pom.xml
xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.sunrise</groupId> <artifactId>clinic</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
File: application.yml in src/main/resources
yamlclinic: name: Sunrise Clinic max-daily-tokens: 60 slot-length: 15m contact: phone: "020-5550123" email: help@sunriseclinic.example departments: - General - Dental - Eye
File: ClinicApplication.java in package com.sunrise.clinic
javapackage com.sunrise.clinic; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; @SpringBootApplication @ConfigurationPropertiesScan public class ClinicApplication { public static void main(String[] args) { SpringApplication.run(ClinicApplication.class, args); } }
File: ClinicProperties.java in package com.sunrise.clinic
javapackage com.sunrise.clinic; import java.time.Duration; import java.util.List; 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.boot.context.properties.bind.DefaultValue; import org.springframework.validation.annotation.Validated; @ConfigurationProperties(prefix = "clinic") @Validated public record ClinicProperties( @NotBlank String name, @Min(1) @Max(200) int maxDailyTokens, @DefaultValue("10m") Duration slotLength, Contact contact, List<String> departments) { public record Contact(String phone, String email) {} }
File: ClinicController.java in package com.sunrise.clinic
javapackage com.sunrise.clinic; import java.util.List; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class ClinicController { record Summary(String name, int tokensPerDay, long slotMinutes, long slotsPerHour, String phone, List<String> departments) {} private final ClinicProperties props; public ClinicController(ClinicProperties props) { this.props = props; } @GetMapping("/clinic") public Summary clinic() { long minutes = props.slotLength().toMinutes(); return new Summary(props.name(), props.maxDailyTokens(), minutes, 60 / minutes, props.contact().phone(), props.departments()); } }
Run it and call the endpoint:
bash./mvnw spring-boot:run curl http://localhost:8080/clinic
Output:
json{ "name": "Sunrise Clinic", "tokensPerDay": 60, "slotMinutes": 15, "slotsPerHour": 4, "phone": "020-5550123", "departments": ["General", "Dental", "Eye"] }
The reply comes on one line; it is spaced out here for reading.
Code Explained
- The prefix
clinicin@ConfigurationPropertiessays that every key starting withclinicbelongs to this record. - A record is a good fit. Its values are set once and never change, which is exactly what settings should do.
@ConfigurationPropertiesScanon the main class finds all such records and registers them as beans. Without it, or without@EnableConfigurationProperties, the record is never created.max-daily-tokensin the file binds tomaxDailyTokens. This is relaxed binding.15mbecomes aDurationof fifteen minutes. If the key were missing,@DefaultValue("10m")would give ten minutes.Contactis a nested record that binds toclinic.contact, and the list binds to the three departments.- The controller receives
ClinicPropertieslike any other bean. It computes four slots per hour from the slot length.
Overriding a Value From Outside
Because the record is bound from the Environment, every source that the environment knows can change it. We started the same jar with the environment variables CLINIC_MAX_DAILY_TOKENS=80 and CLINIC_SLOT_LENGTH=20m. The endpoint then reported 80 tokens and 20 minute slots, with three slots each hour. We also passed --clinic.contact.phone=100 on the command line, and the phone number in the reply changed to 100. The file was not touched in either run.
This is how one build can run in several places. The file carries safe defaults, and each server overrides only what it must.
The Same Settings in a Properties File
If your project uses application.properties, the same group looks like this. The record does not change at all.
propertiesclinic.name=Sunrise Clinic clinic.max-daily-tokens=60 clinic.slot-length=15m clinic.contact.phone=020-5550123 clinic.departments[0]=General clinic.departments[1]=Dental
Nested groups become extra dots, and lists use square brackets with a position number.
Validation in Action
Add @Validated to the class and the validation starter to the project, and the constraints are checked while binding. We changed the token limit in the file from 60 to 500 and started the app again. It did not start. The report said that binding to ClinicProperties failed for the property clinic.maxDailyTokens, with the value 500 and the reason must be less than or equal to 200. It even named the file and the line. Restore 60 and the app runs again.
@ConfigurationProperties or @Value?
| Need | Better choice |
|---|---|
| One single value | @Value |
| A group with a shared prefix | @ConfigurationProperties |
| Nested settings and lists | @ConfigurationProperties |
| Validation of settings | @ConfigurationProperties |
| A quick calculation with SpEL | @Value |
Common Mistakes
- Forgetting the validation starter.
@Validateddoes nothing useful ifspring-boot-starter-validationis missing from the project. - Wrong prefix. A prefix must be lower case with hyphens only, such as
clinicorsunrise-clinic. - Mutable classes without setters. A regular class needs setters to be bound. A record does not.
- Mixing units. Write a unit with durations, such as
15m. A bare15is read as milliseconds by default, which is very short.
Interview Questions
What does `@ConfigurationProperties` do?
Ans:It binds all settings with a common prefix to a Java class or record, with type conversion and optional validation.
How is it different from `@Value`?
Ans:@Value injects one value at a time. @ConfigurationProperties maps a whole group, supports nested objects and lists, and applies relaxed binding.
How do you enable validation on it?
Ans:Add the validation starter, put @Validated on the class, and use constraint annotations like @Min and @NotBlank.
Key Points to Remember
@ConfigurationPropertiesmaps a prefix to a class or record.- Records make clean, unchangeable settings holders.
- Register the type with
@ConfigurationPropertiesScanor@EnableConfigurationProperties. - Relaxed binding lets
max-daily-tokensmatchmaxDailyTokens. @Validatedstops a bad configuration at startup.@DefaultValuesupplies a fallback for missing keys.
Frequently Asked Questions
Is @ConfigurationProperties better than @Value?
For groups of settings, yes. It is easier to read, validate and test. For a single value, @Value is shorter.
Do I need setters or getters?
Not with a record, because Spring binds through the constructor. A normal class needs setters, or constructor binding.
Can I use @ConfigurationProperties with a properties file?
Yes. It works with both application.properties and application.yml, because both fill the same Environment.
Can the values change while the app is running?
No. The object is created once at startup. Restart the app after changing a value.
Related Topics
- @Value Annotation: inject a single setting.
- Externalized Configuration: the sources these properties can come from.
- Bean Validation: the constraint annotations used on the record.
- Spring Profiles: different settings for different environments.
Practice Problems
Try each problem on your own first. The Easy problem uses the same pom.xml as the Sunrise Clinic code above; only change the groupId and artifactId. The Medium problem brings its own pom.xml because it needs the validation starter.
Easy: Library Rules Record
Lakeview Library keeps library.max-books=4, library.loan-days=14 and library.fine-per-day=2 in application.properties. Bind them to a record with the prefix library. Build GET /rules that returns the rules and the fine for a book that is 5 days late.
Show answerHide answer
File: application.properties in src/main/resources
propertieslibrary.max-books=4 library.loan-days=14 library.fine-per-day=2
File: RulesApplication.java in package com.lakeview.rules
javapackage com.lakeview.rules; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; @SpringBootApplication @ConfigurationPropertiesScan public class RulesApplication { public static void main(String[] args) { SpringApplication.run(RulesApplication.class, args); } }
File: LibraryProperties.java in package com.lakeview.rules
javapackage com.lakeview.rules; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "library") public record LibraryProperties(int maxBooks, int loanDays, int finePerDay) {}
File: RulesController.java in package com.lakeview.rules
javapackage com.lakeview.rules; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class RulesController { record Rules(int maxBooks, int loanDays, int fineFor5DaysLate) {} private final LibraryProperties props; public RulesController(LibraryProperties props) { this.props = props; } @GetMapping("/rules") public Rules rules() { return new Rules(props.maxBooks(), props.loanDays(), props.finePerDay() * 5); } }
curl http://localhost:8080/rules prints:
json{ "maxBooks": 4, "loanDays": 14, "fineFor5DaysLate": 10 }
Medium: TiffinBox Order Limits with Validation
TiffinBox, a tiffin service, limits its kitchen. Bind these settings with the prefix tiffin: name, max-orders-per-day (must be at least 1), a nested delivery group with fee and radius-km, and an order-cutoff duration that defaults to 2h when missing. Validate the record so a value of 0 for the daily orders stops the app at startup. Build GET /tiffin that returns a summary.
Show answerHide answer
File: pom.xml
xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.tiffinbox</groupId> <artifactId>orders</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
File: application.yml in src/main/resources
yamltiffin: name: TiffinBox max-orders-per-day: 120 delivery: fee: 25 radius-km: 6
File: OrdersApplication.java in package com.tiffinbox.orders
javapackage com.tiffinbox.orders; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; @SpringBootApplication @ConfigurationPropertiesScan public class OrdersApplication { public static void main(String[] args) { SpringApplication.run(OrdersApplication.class, args); } }
File: TiffinProperties.java in package com.tiffinbox.orders
javapackage com.tiffinbox.orders; import java.time.Duration; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.bind.DefaultValue; import org.springframework.validation.annotation.Validated; @ConfigurationProperties(prefix = "tiffin") @Validated public record TiffinProperties( @NotBlank String name, @Min(1) int maxOrdersPerDay, Delivery delivery, @DefaultValue("2h") Duration orderCutoff) { public record Delivery(int fee, int radiusKm) {} }
File: TiffinController.java in package com.tiffinbox.orders
javapackage com.tiffinbox.orders; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class TiffinController { record Summary(String name, int maxOrders, int deliveryFee, int radiusKm, long cutoffHours) {} private final TiffinProperties props; public TiffinController(TiffinProperties props) { this.props = props; } @GetMapping("/tiffin") public Summary tiffin() { return new Summary(props.name(), props.maxOrdersPerDay(), props.delivery().fee(), props.delivery().radiusKm(), props.orderCutoff().toHours()); } }
curl http://localhost:8080/tiffin prints:
json{ "name": "TiffinBox", "maxOrders": 120, "deliveryFee": 25, "radiusKm": 6, "cutoffHours": 2 }
curl prints the JSON on one line; it is spaced out here for reading. The cutoff was not in the file, so the default of 2 hours is used. We also set max-orders-per-day: 0 and started again. The app stopped at startup with a binding error for the property tiffin.maxOrdersPerDay.