Configuration, Profiles & Actuator

Type-Safe Config with @ConfigurationProperties

18 min Lesson 2 of 13

Type-Safe Config with @ConfigurationProperties

In the previous lesson you saw how to read individual values with @Value. That approach works for one or two properties, but when a feature needs a dozen related settings — timeout, retry count, base URL, credentials — injecting each one separately produces scattered, hard-to-maintain code. Spring Boot solves this with @ConfigurationProperties: a mechanism that binds an entire group of properties to a single, strongly-typed Java class.

Why Prefer @ConfigurationProperties Over @Value

Consider injecting five mail-server settings with @Value:

@Value("${mail.host}") private String host; @Value("${mail.port}") private int port; @Value("${mail.username}") private String username; @Value("${mail.password}") private String password; @Value("${mail.timeout-ms}") private long timeoutMs;

Each annotation is fragile: a typo in the key compiles fine but throws at runtime. There is no IDE auto-complete across the group, no validation, and no single place to see all the settings together. @ConfigurationProperties fixes every one of these issues.

Basic Setup

First, declare a plain Java class annotated with @ConfigurationProperties and a common prefix. In Spring Boot 3 you also need to mark it with @Component (or register it via @EnableConfigurationProperties).

package com.example.demo.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component @ConfigurationProperties(prefix = "mail") public class MailProperties { private String host; private int port = 25; // default value private String username; private String password; private long timeoutMs = 5_000; // standard getters and setters (required for binding) public String getHost() { return host; } public void setHost(String host) { this.host = host; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } public String getUsername() { return username; } public void setUsername(String u) { this.username = u; } public String getPassword() { return password; } public void setPassword(String p) { this.password = p; } public long getTimeoutMs() { return timeoutMs; } public void setTimeoutMs(long t) { this.timeoutMs = t; } }

Then in application.properties (or application.yml):

mail.host=smtp.example.com mail.port=587 mail.username=noreply@example.com mail.password=${MAIL_PASSWORD} mail.timeout-ms=10000
Relaxed binding: Spring Boot maps timeout-ms, timeoutMs, TIMEOUT_MS, and timeout_ms all to the same field. You only need to be consistent within a single source file — the framework normalises the name at bind time.

Using the Properties Bean

Inject the class just like any other Spring bean:

@Service public class MailService { private final MailProperties mail; public MailService(MailProperties mail) { this.mail = mail; } public void send(String to, String subject, String body) { // uses mail.getHost(), mail.getPort(), etc. } }

Because the properties class is a real bean, you get constructor injection, testability, and IDE navigation for free.

Using Java Records (Spring Boot 3.x)

Spring Boot 3 supports binding directly to Java records. Records are immutable by design, which is ideal for configuration that should never change after startup:

package com.example.demo.config; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "mail") public record MailProperties( String host, int port, String username, String password, long timeoutMs ) {}
// Register in @SpringBootApplication or a @Configuration class @SpringBootApplication @EnableConfigurationProperties(MailProperties.class) public class DemoApplication { ... }
Prefer records for immutable config. A record forces you to provide every value at construction time and cannot be accidentally mutated later. This is the recommended style in new Spring Boot 3 code. If you need defaults or optional fields, stick with the mutable-class style.

Nested Properties

Configuration groups can be nested to any depth. Consider a service that talks to an external API with its own retry policy:

@ConfigurationProperties(prefix = "payment") @Component public class PaymentProperties { private String baseUrl; private Retry retry = new Retry(); // getters / setters ... public static class Retry { private int maxAttempts = 3; private long backoffMs = 500; // getters / setters ... } }
payment.base-url=https://api.payments.io payment.retry.max-attempts=5 payment.retry.backoff-ms=1000

Validation with Bean Validation

Add @Validated to the class and use standard Bean Validation annotations on fields. Spring Boot runs validation at startup and fails fast with a clear error if a constraint is violated — rather than blowing up at runtime inside business logic.

import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Positive; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import org.springframework.validation.annotation.Validated; @Component @ConfigurationProperties(prefix = "mail") @Validated public class MailProperties { @NotBlank private String host; @Positive private int port; @NotBlank private String username; // ... }
Missing spring-boot-starter-validation. Bean Validation on config properties requires spring-boot-starter-validation on the classpath. If you annotate fields with @NotBlank but forget the starter, the annotations are silently ignored at startup and you get no validation at all. Always verify the dependency is present.

IDE Metadata Auto-Complete

Add the annotation processor to your pom.xml so IDEs generate auto-complete and documentation for your custom prefixes:

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

At compile time the processor writes META-INF/spring-configuration-metadata.json. IntelliJ IDEA and VS Code read this file to offer property-name completion and inline documentation inside application.properties.

@ConfigurationProperties vs @Value — Decision Guide

  • Use @Value for a single, isolated property — for example a feature flag or a simple timeout that doesn't belong to a logical group.
  • Use @ConfigurationProperties whenever two or more properties share a logical prefix, especially when the group is injected into multiple beans, needs default values, or must be validated at startup.
  • Never mix the two approaches for the same conceptual group — pick one and be consistent.

Summary

@ConfigurationProperties turns a flat key-value namespace into a structured, type-safe object. You gain IDE auto-complete, relaxed binding from any source (properties file, YAML, environment variable, command-line argument), startup validation, and a single place to document every setting a feature requires. In the next lesson you will see how Spring Profiles let you swap entire sets of these properties depending on the environment.