Hire Me
← All Writing Spring Boot

@ConfigurationProperties — Type-Safe Configuration Done Right

How to replace @Value sprawl with @ConfigurationProperties for type-safe, validated, and testable configuration in Spring Boot microservices.

Every Spring Boot project starts clean. A handful of properties in application.yml, a few @Value annotations, everything readable. Then it grows. Six months into a microservices programme you have forty-odd properties scattered across twenty services, @Value("${some.deeply.nested.property.that.no-one-can-remember}") annotations sprinkled through production code, and no consistent way to validate that required configuration is present before the application starts.

I’ve seen this pattern in every large programme I’ve worked on. The fix is straightforward: @ConfigurationProperties. It’s been in Spring Boot since the beginning, it’s well-supported, and it solves the problem completely. Yet I still see @Value used by default on teams that haven’t been shown a better way.

What’s Wrong with @Value

@Value works fine for a single property in a single class. The problems compound as the codebase grows:

// The problem: scattered, unvalidated, hard to test
@Service
public class MarketDataConsumer {

    @Value("${betfair.streaming.reconnect-delay-ms}")
    private long reconnectDelayMs;

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

    @Value("${betfair.streaming.endpoint}")
    private String endpoint;
}

Three related properties, injected separately, validated nowhere. Add ten more services like this and configuration becomes genuinely difficult to reason about.

@ConfigurationProperties — the Right Model

@ConfigurationProperties binds a prefix of your configuration tree to a typed Java class. Related properties live together, the binding is automatic, and you get validation for free via Bean Validation.

@ConfigurationProperties(prefix = "betfair.streaming")
@Validated
public class BetfairStreamingProperties {

    @NotBlank
    private String endpoint;

    @Positive
    private long reconnectDelayMs = 5000;

    @Min(1) @Max(10)
    private int maxRetries = 3;

    @NotNull
    private Duration heartbeatInterval = Duration.ofSeconds(30);

    // getters and setters (or use a record — see below)
}

Register it with @EnableConfigurationProperties on your configuration class or use the @ConfigurationPropertiesScan annotation on your main class, then inject the whole object:

@Service
@RequiredArgsConstructor
public class MarketDataConsumer {

    private final BetfairStreamingProperties streamingProps;

    public void connect() {
        var endpoint = streamingProps.getEndpoint();
        var delay    = streamingProps.getReconnectDelayMs();
        // ...
    }
}

The binding happens at startup. If betfair.streaming.endpoint is missing, the application fails to start with a clear message — not at the point where the property is first used in production.

Using Records for Immutable Configuration

From Spring Boot 2.6, @ConfigurationProperties works cleanly with Java records. Records are immutable by default, which is exactly what you want from configuration — it shouldn’t change after the application starts.

@ConfigurationProperties(prefix = "betfair.streaming")
public record BetfairStreamingProperties(

    @NotBlank
    String endpoint,

    @Positive
    long reconnectDelayMs,

    @Min(1) @Max(10)
    int maxRetries,

    @NotNull
    Duration heartbeatInterval
) {
    public BetfairStreamingProperties {
        if (reconnectDelayMs > 30_000) {
            throw new IllegalArgumentException(
                "reconnectDelayMs exceeds maximum safe value of 30000ms"
            );
        }
    }
}

The compact constructor lets you add cross-field validation that Bean Validation annotations can’t express. Immutability means you can inject the record as a constructor parameter and be certain its values won’t change under you.

Nested Properties

@ConfigurationProperties handles nested structures naturally, which is where it really pulls ahead of @Value:

betfair:
  streaming:
    endpoint: "stream-api-integration.betfair.com"
    reconnect-delay-ms: 5000
    max-retries: 3
    heartbeat-interval: 30s
  exchange:
    base-url: "https://api.betfair.com/exchange/"
    timeout: 10s
    rate-limit:
      requests-per-second: 20
      burst-capacity: 5
@ConfigurationProperties(prefix = "betfair")
@Validated
public class BetfairProperties {

    @Valid @NotNull
    private StreamingProperties streaming = new StreamingProperties();

    @Valid @NotNull
    private ExchangeProperties exchange = new ExchangeProperties();

    public record StreamingProperties(
        @NotBlank String endpoint,
        @Positive long reconnectDelayMs,
        @Min(1) int maxRetries
    ) {}

    public record ExchangeProperties(
        @NotBlank String baseUrl,
        @NotNull Duration timeout,
        @Valid RateLimitProperties rateLimit
    ) {}

    public record RateLimitProperties(
        @Positive int requestsPerSecond,
        @Positive int burstCapacity
    ) {}
}

The @Valid annotation on nested types propagates Bean Validation into the nested structure. The full configuration tree is validated at startup, completely typed, and accessible from a single injected object.

Testing

Unit testing with @ConfigurationProperties is straightforward. No Spring context required:

@Test
void shouldRejectZeroReconnectDelay() {
    var props = new BetfairStreamingProperties(
        "stream-api-integration.betfair.com",
        0L,   // invalid — @Positive
        3,
        Duration.ofSeconds(30)
    );

    var validator = Validation.buildDefaultValidatorFactory().getValidator();
    var violations = validator.validate(props);

    assertThat(violations).hasSize(1);
    assertThat(violations.iterator().next().getPropertyPath().toString())
        .isEqualTo("reconnectDelayMs");
}

For integration tests, @SpringBootTest with a test application.yml works exactly as you’d expect — the properties class is bound and validated as part of the context startup.

ProTip: Generate Configuration Metadata

Add the spring-boot-configuration-processor annotation processor to your build and Spring Boot will generate spring-configuration-metadata.json at compile time. This gives you IDE autocompletion and documentation for your custom properties in application.yml — the same experience as first-party Spring Boot properties.

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

Add a @Description annotation to your properties class and that description appears in the IDE hint. Small thing, meaningful improvement for anyone else maintaining the service.

The Rule

Use @Value for a single, isolated property that genuinely has no related siblings and needs no validation. Use @ConfigurationProperties for everything else. The threshold in practice is: if you have two or more properties with the same prefix, they belong in a properties class.

On the DWP Digital programme, every service had a properties class for each external integration — DWP upstream services, AWS configuration, Kafka — and none of them used @Value. The result was configuration that was easy to review, easy to validate in environment-specific deployments, and genuinely fast to onboard new engineers to.

If you’re building Spring Boot microservices that need to be reliably configured across multiple environments, get in touch.