00:00

Spring Boot Configuration Properties

@ConfigurationProperties is a Spring Boot feature that binds external configuration values, from application.properties, YAML files, environment variables or command-line arguments, to strongly typed Java objects.

In this guide you will build the configuration for an online shop step by step: a simple properties class, immutable records, nested objects, lists and maps, durations and sizes, default values, validation, environment-specific values, IDE auto-completion and tests.

1. What Is @ConfigurationProperties?

Every real application has settings that change between environments: database addresses, API keys, timeouts, feature switches. These belong outside the code, in configuration files or environment variables.

@ConfigurationProperties lets you map a group of related settings that share a common prefix to one Java class. Instead of reading values one at a time, Spring Boot fills the whole object for you, converting each value to the right type.

Think of it like a form. Reading settings one by one with @Value is like copying single answers from a form. @ConfigurationProperties reads the whole form into a neat object that you can pass around.

2. @Value vs @ConfigurationProperties

Spring offers two ways to read configuration. Both are useful, but for different jobs.

Using @Value
@Service
public class CheckoutService {

    @Value("${shop.payment.timeout}")
    private Duration paymentTimeout;

    @Value("${shop.payment.max-retries}")
    private int maxRetries;

    @Value("${shop.free-shipping-over}")
    private BigDecimal freeShippingOver;
}

This works, but the property names are scattered across the code as strings, a typo is only found when the application starts, and nothing tells you which settings belong together.

Aspect@Value@ConfigurationProperties
Best forOne or two single valuesA group of related values
Where names liveSpread across classes as stringsIn one class, in one place
Type conversionYesYes, including nested objects, lists and maps
Relaxed binding (max-retries, maxRetries, MAX_RETRIES)LimitedFull support
Validation with @NotBlank, @Min and similarNoYes
IDE auto-completion for your own propertiesNoYes, with the configuration processor
SpEL expressions such as #{...}YesNo
Easy to unit testHarderEasy: it is a plain object

Rule of thumb: use @ConfigurationProperties for anything that has more than one or two related settings, and keep @Value for the occasional single value.

3. Your First Configuration Properties Class

Start with the settings in application.properties:

application.properties
shop.name=Book Corner
shop.support-email=help@bookcorner.example
shop.max-items-per-order=20

Now create a class whose fields match the names after the shop. prefix. In this classic JavaBean style, Spring creates the object and calls the setters:

ShopProperties.java
package com.example.shop.config;

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

@ConfigurationProperties(prefix = "shop")
public class ShopProperties {

    private String name;
    private String supportEmail;     // bound from shop.support-email
    private int maxItemsPerOrder;    // bound from shop.max-items-per-order

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSupportEmail() {
        return supportEmail;
    }

    public void setSupportEmail(String supportEmail) {
        this.supportEmail = supportEmail;
    }

    public int getMaxItemsPerOrder() {
        return maxItemsPerOrder;
    }

    public void setMaxItemsPerOrder(int maxItemsPerOrder) {
        this.maxItemsPerOrder = maxItemsPerOrder;
    }
}

Notice how support-email in the file becomes supportEmail in Java. Spring Boot matches the two automatically; you will learn more in section 8. It also converts "20" into the int 20.

The class is not used yet: Spring Boot must be told to create it. That is the next step.

4. Registering Your Properties Class

There are three ways to turn a @ConfigurationProperties class into a bean. Pick one.

4.1 Scan for them (recommended)

Add @ConfigurationPropertiesScan to your main class. Spring Boot then finds every @ConfigurationProperties class in that package and below:

ShopApplication.java
package com.example.shop;

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

@SpringBootApplication
@ConfigurationPropertiesScan
public class ShopApplication {

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

4.2 List them explicitly

@EnableConfigurationProperties registers only the classes you name. This is common in libraries and auto-configuration, where you want full control:

ShopConfig.java
@Configuration
@EnableConfigurationProperties(ShopProperties.class)
public class ShopConfig {
}

4.3 Add @Component

Putting @Component on a JavaBean-style properties class also works, because component scanning picks it up. It does not work for records or constructor-bound classes, so the first two options are better habits.

5. Immutable Properties with Records

Configuration should not change while the application runs. A Java record gives you an immutable properties class in a few lines: no setters, no getters to write, and nobody can change the values by accident.

ShopProperties.java
package com.example.shop.config;

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

@ConfigurationProperties(prefix = "shop")
public record ShopProperties(
        String name,
        String supportEmail,
        int maxItemsPerOrder) {
}

Spring Boot sees that the record has a single constructor and passes the values to it. This is called constructor binding. You read values with the record’s accessor methods, such as properties.name().

A normal class works the same way when it has exactly one constructor with parameters. If a class has several constructors, put @ConstructorBinding on the one Spring should use:

PaymentProperties.java
package com.example.shop.config;

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

@ConfigurationProperties(prefix = "shop.payment")
public class PaymentProperties {

    private final String provider;
    private final int maxRetries;

    @ConstructorBinding
    public PaymentProperties(String provider, int maxRetries) {
        this.provider = provider;
        this.maxRetries = maxRetries;
    }

    // Used by tests, not by Spring
    public PaymentProperties(String provider) {
        this(provider, 3);
    }

    public String getProvider() {
        return provider;
    }

    public int getMaxRetries() {
        return maxRetries;
    }
}
Version note: In Spring Boot 2.x, @ConstructorBinding was placed on the class and lived in a different package. Since Spring Boot 3.0 it is only needed on a constructor, and only when there is more than one.

6. Nested Objects, Lists and Maps

Real configuration has structure. Records can contain other records, lists and maps, and Spring Boot fills all of them.

application.properties
shop.name=Book Corner
shop.support-email=help@bookcorner.example

# Nested object
shop.payment.provider=stripe
shop.payment.max-retries=3

# List of simple values
shop.currencies=EUR,USD,GBP

# List of objects
shop.warehouses[0].city=Berlin
shop.warehouses[0].capacity=5000
shop.warehouses[1].city=Madrid
shop.warehouses[1].capacity=3000

# Map: country code -> tax rate
shop.tax-rates.de=0.19
shop.tax-rates.fr=0.20
ShopProperties.java
package com.example.shop.config;

import java.math.BigDecimal;
import java.util.List;
import java.util.Map;

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

@ConfigurationProperties(prefix = "shop")
public record ShopProperties(
        String name,
        String supportEmail,
        Payment payment,
        List<String> currencies,
        List<Warehouse> warehouses,
        Map<String, BigDecimal> taxRates) {

    public record Payment(String provider, int maxRetries) {
    }

    public record Warehouse(String city, int capacity) {
    }
}

Keeping the nested records inside ShopProperties shows clearly that they belong to it. A comma-separated value such as EUR,USD,GBP is split into a list automatically.

7. Using YAML

For nested settings, YAML is often easier to read, because the structure is visible from the indentation. Here is the same configuration in application.yml:

application.yml
shop:
  name: Book Corner
  support-email: help@bookcorner.example
  payment:
    provider: stripe
    max-retries: 3
  currencies:
    - EUR
    - USD
    - GBP
  warehouses:
    - city: Berlin
      capacity: 5000
    - city: Madrid
      capacity: 3000
  tax-rates:
    de: 0.19
    fr: 0.20

The Java class does not change at all. Choose whichever format your team prefers, and avoid having both application.properties and application.yml in the same project, because it becomes hard to know which value wins.

Tip: YAML is sensitive to indentation and treats some values specially. For example, version: 1.10 is read as the number 1.1. Put such values in quotes: version: "1.10".

8. Relaxed Binding and Environment Variables

Spring Boot is flexible about how property names are written. All of these bind to the field supportEmail under the shop prefix:

Written asStyleTypical place
shop.support-emailKebab case (recommended).properties and .yml files
shop.supportEmailCamel caseFiles (works, but less common)
shop.support_emailUnderscoreFiles (works, but less common)
SHOP_SUPPORTEMAILUpper case with underscoresEnvironment variables

This is called relaxed binding. It is what lets you override any setting with an environment variable, which is how most cloud platforms and containers pass configuration.

To turn a property name into an environment variable:

  1. Replace each dot (.) with an underscore (_).
  2. Remove any dashes (-).
  3. Convert everything to upper case.
Terminal
# Overrides shop.payment.max-retries
export SHOP_PAYMENT_MAXRETRIES=5

# Overrides shop.support-email
export SHOP_SUPPORTEMAIL=support@bookcorner.example

java -jar shop.jar

You can also override a value from the command line:

Terminal
java -jar shop.jar --shop.payment.max-retries=5
Important: The prefix itself must be in lower-case kebab case, such as "shop" or "shop.payment". A prefix like "myShop" is rejected when the application starts.

9. Durations, Data Sizes and Enums

Spring Boot converts text into many useful types for you. Three are especially handy.

9.1 Durations

Use java.time.Duration for timeouts and intervals, and write the value with a unit:

application.properties
shop.payment.timeout=10s
shop.cart.expiry=2h
shop.cache.refresh-interval=500ms
SuffixUnitSuffixUnit
nsnanosecondssseconds
usmicrosecondsmminutes
msmillisecondshhours
ddays

A plain number without a unit, such as timeout=10, is read as milliseconds. To make plain numbers mean seconds, add @DurationUnit(ChronoUnit.SECONDS) to the field or parameter. Writing the unit is clearer, though.

9.2 Data sizes

Use DataSize for file sizes and memory limits:

application.properties
shop.uploads.max-image-size=5MB
shop.uploads.max-total-size=1GB

Supported units are B, KB, MB, GB and TB. A plain number means bytes, unless you add @DataSizeUnit.

9.3 Enums

Enum values are matched without caring about case or dashes, so stripe, STRIPE and Stripe all bind to PaymentProvider.STRIPE.

Here is the payment record using all three types:

ShopProperties.java (part)
import java.time.Duration;

import org.springframework.util.unit.DataSize;

public record Payment(
        PaymentProvider provider,   // stripe -> PaymentProvider.STRIPE
        int maxRetries,
        Duration timeout,           // 10s -> Duration.ofSeconds(10)
        DataSize maxReceiptSize) {  // 2MB -> DataSize.ofMegabytes(2)
}

public enum PaymentProvider {
    STRIPE, PAYPAL, BANK_TRANSFER
}

10. Default Values

When a property is missing from the configuration, an object field is null and a number is 0. Usually you want a sensible default instead.

10.1 Defaults in a record: @DefaultValue

ShopProperties.java
package com.example.shop.config;

import java.time.Duration;
import java.util.List;

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

@ConfigurationProperties(prefix = "shop")
public record ShopProperties(
        String name,
        @DefaultValue("20") int maxItemsPerOrder,
        @DefaultValue({"EUR", "USD"}) List<String> currencies,
        @DefaultValue Payment payment) {   // creates Payment even if no shop.payment.* is set

    public record Payment(
            @DefaultValue("3") int maxRetries,
            @DefaultValue("10s") Duration timeout) {
    }
}

The empty @DefaultValue on payment is worth remembering: without it, the whole Payment object would be null if no shop.payment.* property is set at all.

10.2 Defaults in a JavaBean class

In a class with setters, simply give the field an initial value. It is replaced only when the property is set:

ShopProperties.java (part)
private int maxItemsPerOrder = 20;
private Duration paymentTimeout = Duration.ofSeconds(10);
private List<String> currencies = new ArrayList<>(List.of("EUR", "USD"));

11. Validating Properties at Startup

A missing database address or a negative timeout should stop the application at startup, not cause a confusing error hours later. Add the validation starter:

pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Then put @Validated on the class and use the standard jakarta.validation annotations:

ShopProperties.java
package com.example.shop.config;

import java.time.Duration;

import jakarta.validation.Valid;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.validation.annotation.Validated;

@Validated
@ConfigurationProperties(prefix = "shop")
public record ShopProperties(
        @NotBlank String name,
        @Email @NotBlank String supportEmail,
        @Min(1) @Max(100) @DefaultValue("20") int maxItemsPerOrder,
        @Valid @DefaultValue Payment payment) {   // @Valid also checks Payment

    public record Payment(
            @NotBlank String provider,
            @Min(0) @Max(10) int maxRetries,
            @NotNull @DefaultValue("10s") Duration timeout) {
    }
}

If a value breaks a rule, the application refuses to start and tells you exactly what is wrong:

Console output (shortened)
***************************
APPLICATION FAILED TO START
***************************

Description:

Binding to target com.example.shop.config.ShopProperties failed:

    Property: shop.supportEmail
    Value: "help-at-bookcorner"
    Reason: must be a well-formed email address

Action:

Update your application's configuration
Note: Older tutorials use javax.validation. Spring Boot 3 and later use the jakarta.validation package instead. Learn more in the Spring Boot Validation guide.

12. Using Properties in Your Code

A properties class is a normal bean, so inject it through the constructor wherever you need it. This example uses the validated ShopProperties record from section 11:

CheckoutService.java
package com.example.shop.order;

import org.springframework.stereotype.Service;

import com.example.shop.config.ShopProperties;

@Service
public class CheckoutService {

    private final ShopProperties shop;

    public CheckoutService(ShopProperties shop) {
        this.shop = shop;
    }

    public void checkLimits(Cart cart) {
        if (cart.itemCount() > shop.maxItemsPerOrder()) {
            throw new IllegalArgumentException(
                    "You can order at most " + shop.maxItemsPerOrder() + " items");
        }
    }

    public String helpMessage() {
        return "Questions about your order? Write to " + shop.supportEmail();
    }
}

You can also inject it into a @Bean method to configure other objects, such as an HTTP client:

PaymentClientConfig.java
@Configuration
public class PaymentClientConfig {

    @Bean
    public RestClient paymentRestClient(RestClient.Builder builder, ShopProperties shop) {
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(shop.payment().timeout());
        factory.setReadTimeout(shop.payment().timeout());

        return builder
                .baseUrl("https://api." + shop.payment().provider() + ".example")
                .requestFactory(factory)
                .build();
    }
}

Pass only the settings a class really needs. A service that only needs the payment settings can receive shop.payment() rather than the whole ShopProperties object.

13. Different Values for Each Environment

Your local machine, test server and production need different values. Put shared values in application.properties and only the differences in profile-specific files:

application.properties
shop.name=Book Corner
shop.payment.provider=stripe
shop.payment.timeout=10s
application-prod.properties
shop.payment.timeout=5s
shop.payment.max-retries=5

Start the application with --spring.profiles.active=prod, and the values from application-prod.properties replace the matching shared ones. Your ShopProperties class does not change at all. See Spring Boot Profiles for the full picture.

13.1 Which value wins?

When the same property is set in several places, Spring Boot uses this order. Higher in the list wins:

  1. Command-line arguments, such as --shop.payment.timeout=3s
  2. Environment variables, such as SHOP_PAYMENT_TIMEOUT=3s
  3. Profile-specific files, such as application-prod.properties
  4. application.properties or application.yml
  5. Defaults in your class, such as @DefaultValue

This is why secrets are usually passed as environment variables: they override the file without ever being written into it.

14. IDE Auto-Completion with the Configuration Processor

Spring Boot’s own properties come with auto-completion in IntelliJ IDEA, Eclipse and VS Code. You can get the same for your own properties by adding the configuration processor:

pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

For Gradle:

build.gradle
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'

When you build the project, the processor writes a file called META-INF/spring-configuration-metadata.json that describes every property: its name, type, default value and description. Your IDE reads it and then:

  • Suggests shop.payment.max-retries as you type.
  • Shows the description from the field’s Javadoc comment.
  • Warns you about unknown or misspelled property names.
ShopProperties.java (JavaBean style)
@ConfigurationProperties(prefix = "shop")
public class ShopProperties {

    /**
     * Largest number of items a customer can put in a single order.
     */
    private int maxItemsPerOrder = 20;

    // getter and setter
}

Rebuild the project after changing a properties class, so the metadata file is updated.

15. Binding Properties to Third-Party Classes

Sometimes you want to configure a class you cannot change, such as a connection pool from a library. Put @ConfigurationProperties on a @Bean method, and Spring binds the properties to the object it returns by calling its setters:

ReportingDataSourceConfig.java
package com.example.shop.config;

import com.zaxxer.hikari.HikariDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ReportingDataSourceConfig {

    @Bean
    @ConfigurationProperties(prefix = "shop.reporting.datasource")
    public HikariDataSource reportingDataSource() {
        return DataSourceBuilder.create()
                .type(HikariDataSource.class)
                .build();
    }
}
application.properties
shop.reporting.datasource.jdbc-url=jdbc:postgresql://reports-db:5432/reports
shop.reporting.datasource.username=reporter
shop.reporting.datasource.maximum-pool-size=5

The property names must match the setters of the returned class. HikariDataSource has setJdbcUrl and setMaximumPoolSize, so the properties are jdbc-url and maximum-pool-size.

Return the concrete type, here HikariDataSource, rather than the DataSource interface, so Spring can see the setters it should call.

16. Testing Configuration Properties

16.1 Using a record directly in unit tests

Because a record is a plain object, a unit test can simply create one. No Spring context is needed. This test uses the record from section 11:

CheckoutServiceTest.java
@Test
void rejectsOrdersAboveTheLimit() {
    ShopProperties shop = new ShopProperties(
            "Test Shop", "test@example.com", 2, null);
    CheckoutService service = new CheckoutService(shop);

    assertThatThrownBy(() -> service.checkLimits(cartWithItems(3)))
            .isInstanceOf(IllegalArgumentException.class);
}

16.2 Testing the binding itself

To check that real property values bind and validate correctly, use ApplicationContextRunner. It starts a tiny Spring context in milliseconds:

ShopPropertiesTest.java
package com.example.shop.config;

import static org.assertj.core.api.Assertions.assertThat;

import java.time.Duration;

import org.junit.jupiter.api.Test;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;

class ShopPropertiesTest {

    private final ApplicationContextRunner runner = new ApplicationContextRunner()
            .withUserConfiguration(TestConfig.class);

    @Test
    void bindsValuesAndAppliesDefaults() {
        runner.withPropertyValues(
                        "shop.name=Book Corner",
                        "shop.support-email=help@bookcorner.example",
                        "shop.payment.provider=stripe")
              .run(context -> {
                  ShopProperties shop = context.getBean(ShopProperties.class);

                  assertThat(shop.name()).isEqualTo("Book Corner");
                  assertThat(shop.maxItemsPerOrder()).isEqualTo(20);   // default
                  assertThat(shop.payment().timeout()).isEqualTo(Duration.ofSeconds(10));
              });
    }

    @Test
    void failsToStartWithAnInvalidEmail() {
        runner.withPropertyValues(
                        "shop.name=Book Corner",
                        "shop.support-email=not-an-email",
                        "shop.payment.provider=stripe")
              .run(context -> assertThat(context).hasFailed());
    }

    @EnableConfigurationProperties(ShopProperties.class)
    static class TestConfig {
    }
}

For tests that need the full application, use @SpringBootTest(properties = "shop.max-items-per-order=5") to override individual values. See the Spring Boot Testing guide for more.

17. Best Practices

  • Group related settings under one clear prefix, such as shop.payment, and give each group its own class or nested record.
  • Prefer records (or constructor binding) so configuration cannot change at runtime.
  • Use @ConfigurationPropertiesScan on the main class instead of @Component on each properties class.
  • Validate required values and ranges with @Validated, so bad configuration fails fast at startup.
  • Use real types: Duration, DataSize, enums and BigDecimal instead of strings and plain numbers.
  • Give sensible defaults so the application works with minimal configuration.
  • Write property names in kebab case (max-retries) in your files.
  • Never commit secrets. Pass passwords and API keys through environment variables or a secret manager.
  • Add the configuration processor and Javadoc comments so your team gets auto-completion and descriptions.
  • Keep business logic out of properties classes; they should only hold values.

18. Common Mistakes and Troubleshooting

ProblemLikely causeFix
All values are null or 0The class was never registered as a bean, or the prefix does not match the fileAdd @ConfigurationPropertiesScan and check the prefix spelling
JavaBean class stays emptySetters are missingAdd setters, or switch to a record
A nested record is nullNo property under that nested prefix is setAdd an empty @DefaultValue to the nested parameter
Startup error about an invalid prefixThe prefix uses camel case, such as myShopUse lower-case kebab case: my-shop
Validation annotations are ignored@Validated or the validation starter is missingAdd both; add @Valid for nested objects
@ConstructorBinding does not compileThe Spring Boot 2 import is usedUse org.springframework.boot.context.properties.bind.ConstructorBinding on a constructor, or remove it
A timeout is far too shorttimeout=10 was read as 10 millisecondsWrite the unit: 10s
An environment variable is ignoredIts name does not follow the conversion rulesUse upper case, underscores for dots, no dashes: SHOP_PAYMENT_MAXRETRIES
No auto-completion in the IDEThe configuration processor is missing or the project was not rebuiltAdd the processor and rebuild

19. Summary

@ConfigurationProperties is the recommended way to manage external configuration in Spring Boot. It gathers related settings into one type-safe class, converts values to the right types, supports nested objects, lists and maps, and accepts names in many styles, so environment variables work out of the box. Use records for immutable configuration, add defaults and validation so mistakes are caught at startup, use profiles for environment-specific values, and add the configuration processor for IDE auto-completion. The result is configuration code that is clean, safe, easy to test and ready for production.