Spring Boot Configuration Properties: @ConfigurationProperties and Relaxed Binding
Master Spring Boot external configuration with @ConfigurationProperties, relaxed binding rules, and application.yml/yml setup.
Master Spring Boot external configuration with @ConfigurationProperties, relaxed binding rules, and application.yml/yml setup. The guide uses practical examples to explain when to use @configurationproperties vs @value, defining @configurationproperties classes and shows how to apply the ideas in a Spring Boot project. It closes with common pitfalls and production checks so you can apply the pattern with fewer surprises.
Spring Boot Configuration Properties
Introduction
Spring Boot’s externalized configuration system is one of its most powerful features. It lets you decouple your application code from environment-specific values, so the same JAR runs everywhere—from a developer’s laptop to production Kubernetes clusters.
Instead of hardcoding database URLs, feature flags, or third-party API keys, you inject them. Spring Boot’s configuration binding connects application.yml properties directly to typed Java objects through @ConfigurationProperties. The framework handles the plumbing, the conversion, and the relaxed binding between kebab-case YAML keys and camelCase Java fields.
This post walks through the two main approaches, how relaxed binding works under the hood, the pitfalls that trip most teams, and what a production-ready configuration setup actually looks like.
When to Use @ConfigurationProperties vs @Value
Spring gives you two primary ways to inject configuration: @ConfigurationProperties and @Value. They serve different purposes.
Use @ConfigurationProperties when you need:
- Multiple related configuration values grouped into a typed object
- Validation with JSR-380 (
@NotNull,@Min,@Max, etc.) - Default values without ceremony
- Spring Boot’s relaxed binding between YAML/environment variables and Java fields
- IDE autocomplete on configuration keys via metadata generation
Use @Value when you need:
- A single, isolated value injected into a field
- SpEL expressions (
#{someBean.method()}) - Quick prototyping with no intention to expand
The performance difference is negligible—both resolve at startup. The maintenance difference is not. A class with 15 @Value annotations is harder to read and test than a single @ConfigurationProperties class with 15 fields.
Defining @ConfigurationProperties Classes
Annotate a class with @ConfigurationProperties and give it a prefix. Spring Boot will bind any matching properties to its fields.
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "app.mailing")
public class MailingProperties {
private String host = "localhost";
private int port = 587;
private @DefaultValue("noreply@example.com") String fromAddress;
private boolean enabled = true;
// 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 getFromAddress() { return fromAddress; }
public void setFromAddress(String fromAddress) { this.fromAddress = fromAddress; }
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
}
You can also use @EnableConfigurationProperties on a @Configuration class instead of @Component—useful when you want to keep configuration binding scoped to a specific module:
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(MailingProperties.class)
public class MailingConfiguration {
// MailingProperties is now a bean in this configuration context
}
application.yml / application.yaml Structure
Spring Boot automatically loads application.yml (or application.yaml) from several locations, in order of precedence (highest wins):
graph TD
A["Command-line args<br/>--spring.config.location"] --> B["OS environment variables<br/>SPRING_APPLICATION_JSON"]
B --> C["java:comp/env JNDI"]
C --> D["Application JAR<br/>application.yml"]
D --> E["Profile-specific<br/>application-{profile}.yml"]
E --> F["Application JAR<br/>application-{profile}.yml"]
F --> G["Locations from<br/>spring.config.additional-location"]
G --> H["Default fallback<br/>classpath application.yml"]
Properties can also be split across profiles. If you activate the prod profile, Spring merges application.yml with application-prod.yml, with profile-specific values overriding the base file.
Example application.yml
spring:
application:
name: user-service
datasource:
url: jdbc:postgresql://localhost:5432/users
username: ${DB_USER:appuser}
password: ${DB_PASSWORD}
driver-class-name: org.postgresql.Driver
hikari:
maximum-pool-size: 20
connection-timeout: 30000
server:
port: ${APP_PORT:8080}
servlet:
context-path: /api
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: when-authorized
app:
mailing:
host: smtp.example.com
port: 587
from-address: noreply@example.com
enabled: true
feature-flags:
new-checkout-flow: ${FF_NEW_CHECKOUT:false}
beta-search: ${FF_BETA_SEARCH:false}
Property Placeholders and Default Values
Use ${VAR_NAME:default} syntax for environment variable substitution with fallbacks. The syntax is property: ${ENV_VAR:fallback}.
The colon-separated default works at every level—datasource URL, server port, feature flags, anything. This makes it easy to externalize secrets to environment variables while providing safe development defaults.
Relaxed Binding
Spring Boot’s relaxed binding handles multiple naming conventions automatically, which is useful when your YAML comes from different sources.
| YAML / Environment Format | Java Field (camelCase) |
|---|---|
app.mailing.host |
app.mailing.host |
app.mailing.host-name |
app.mailing.hostName |
APP_MAILING_HOST |
app.mailing.host |
app.mailing.HOST |
app.mailing.host (case-insensitive) |
This means all of these are equivalent:
# YAML - kebab-case
app:
mailing:
from-address: noreply@example.com
# properties file - kebab-case
app.mailing.from-address=noreply@example.com
# Environment variable - SCREAMING_SNAKE_CASE
APP_MAILING_FROM_ADDRESS=noreply@example.com
Spring Boot normalizes everything when binding. You can confidently use whichever format fits your team’s conventions or platform constraints. Kubernetes ConfigMap keys are typically kebab-case, so APP_MAILING_FROM_ADDRESS maps cleanly to app.mailing.from-address.
@Name and @DefaultValue Annotations
When your YAML key doesn’t match the Java field name, use @Name to explicit map:
import org.springframework.boot.context.properties.bind.Name;
import org.springframework.boot.context.properties.bind.DefaultValue;
@ConfigurationProperties(prefix = "app.mailing")
public class MailingProperties {
@Name("from-email")
@DefaultValue("noreply@example.com")
private String fromAddress;
}
This resolves the mismatch between from-email in YAML and fromAddress in Java without renaming either.
Validation and Type Conversion
Spring Boot automatically converts YAML values to the target field type. String, int, long, boolean, Duration, InetAddress, and many others work out of the box. For custom types, you can register a Converter bean.
JSR-380 validation adds a layer of safety:
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
@ConfigurationProperties(prefix = "app.mailing")
@Validated
public class MailingProperties {
@NotBlank
private String host;
@Min(1)
@Max(65535)
private int port = 587;
@Email
private String fromAddress;
}
When validation fails, Spring Boot prevents the application from starting and logs a clear BindValidationException. This catches misconfiguration at startup rather than at the first request.
Configuration Properties in Practice
Injecting into a Service
import org.springframework.stereotype.Service;
@Service
public class NotificationService {
private final MailingProperties mailing;
public NotificationService(MailingProperties mailing) {
this.mailing = mailing; // constructor injection for testability
}
public void sendWelcomeEmail(String to) {
if (!mailing.isEnabled()) {
return;
}
// use mailing.getHost(), mailing.getPort(), etc.
}
}
Constructor injection (instead of field injection) makes the service unit-testable—you can mock MailingProperties without Spring’s reflection-based injection.
Multi-Environment Configuration
# application.yml (base)
app:
mailing:
host: localhost
port: 587
enabled: false # off by default
---
# application-dev.yml (profile-specific overrides)
spring:
datasource:
url: jdbc:postgresql://localhost:5432/devdb
app:
mailing:
enabled: true
from-address: dev@example.com
Activate with –spring.profiles.active=dev or SPRING_PROFILES_ACTIVE=dev. Profile-specific files deep-merge with the base—only the fields that differ need to appear in the profile file.
Production Failure Scenarios
These are the errors I see most often when configuration binding goes wrong in production.
BindingValidationException at startup
- Cause: A YAML property fails JSR-380 validation. Check the startup logs for the specific field and constraint violation.
- Fix: Either fix the YAML value, adjust the validation constraint, or remove
@Validatedif you need looser behavior during migration.
A property that should be bound but isn’t
- Cause: The YAML key doesn’t match the
@ConfigurationPropertiesprefix. Spring Boot doesn’t warn you about unused keys—it binds what it finds and ignores the rest silently. - Fix: Enable DEBUG logging (
logging.level.org.springframework.boot.context.properties=DEBUG) to see which properties are being bound.
NoSuchBeanException for a @ConfigurationProperties class
- Cause: The class isn’t registered as a Spring bean. It needs either
@Componenton the class or@EnableConfigurationProperties(MyProperties.class)on a@Configurationclass. - Fix: Add one of those two annotations.
Environment variables that seem to have no effect
- Cause: Double underscores in environment variable names (
APPMAILINGHOST) get parsed as kebab-case separators, not literal underscores. Single underscores work correctly. Typos also happen. - Fix: Check the variable name.
APP_MAILING_HOSTmaps toapp.mailing.host.APPMAILINGHOSTdoes not.
Observability Checklist
- Confirm all
@ConfigurationPropertiesclasses are registered as beans (via@Componentor@EnableConfigurationProperties) - Add JSR-380 validation to catch misconfiguration at startup
- Enable DEBUG logging for property binding during development:
logging.level.org.springframework.boot.context.properties=DEBUG - Verify
spring-boot-configuration-processoris added to generate IDE metadata - Protect
/envand/configpropsactuator endpoints with authentication - Document all custom property prefixes in a README or architecture doc
- Ensure all secrets are injected via environment variables, not hardcoded defaults
- Use
ApplicationContextRunnerto unit test property binding for critical configuration classes
Quick Recap Checklist
-
@ConfigurationPropertiesprovides type-safe, validated, relaxed-binding configuration - Use
@Componentor@EnableConfigurationPropertiesto register the binding class as a bean - YAML keys in kebab-case (
from-address) map to camelCase Java fields (fromAddress) - Environment variables use SCREAMING_SNAKE_CASE with underscores as separators
- Use
@Validated+ JSR-380 annotations for startup-time validation - Use
@Namewhen YAML key and Java field name cannot be aligned via conventions - Use
@DefaultValuefor safe fallbacks, but never for secrets - Constructor injection of
@ConfigurationPropertiesmakes services unit-testable - Profile-specific YAML files deep-merge with the base
application.yml -
ApplicationContextRunnerwithEnableConfigurationPropertiesis the standard test pattern
Trade-off Table
| Approach | Pros | Cons |
|---|---|---|
@ConfigurationProperties + class |
Type-safe, validatable, relaxed binding, IDE autocomplete | Slight boilerplate for getters/setters |
@Value annotations |
Quick, no extra class needed | No validation, poor testability, no relaxed binding |
Raw Environment API |
Maximum flexibility | Verbose, string-based, error-prone |
| Profile-specific YAML | Clean environment separation | Can lead to configuration duplication across profiles |
Implementation Snippets
Generating Configuration Metadata
Add the annotation processor to pom.xml or build.gradle to get IDE autocomplete:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
With this processor active, running a build generates META-INF/spring-configuration-metadata.json. IDEs like IntelliJ IDEA use this file to provide autocomplete and documentation for your custom properties.
Testing Configuration Properties
import org.junit.jupiter.api.Test;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
class MailingPropertiesTest {
private final ApplicationContextRunner runner = new ApplicationContextRunner()
.withPropertyValues("app.mailing.host=smtp.test.com", "app.mailing.port=1025")
.withConfiguration(EnableConfigurationProperties(MailingProperties.class));
@Test
void bindsPropertiesCorrectly() {
runner.run(context -> {
MailingProperties props = context.getBean(MailingProperties.class);
assertThat(props.getHost()).isEqualTo("smtp.test.com");
assertThat(props.getPort()).isEqualTo(1025);
});
}
}
ApplicationContextRunner with @EnableConfigurationProperties is the cleanest way to unit test property binding without a full Spring Boot application context.
Security Notes
- Never commit secrets to
application.ymlin version control. Use environment variables (${DB_PASSWORD}) or a secrets manager (HashiCorp Vault, AWS Secrets Manager, Kubernetes Secrets). @DefaultValueon password fields: If you use@DefaultValue(“password”)on a password field and that YAML is committed, it becomes a security incident. Default values for secrets should be empty or throw an exception if not overridden.- Sensitive values in actuator
/envendpoint: Spring Boot Actuator’s/envendpoint exposes configuration property values. Protect it with Spring Security or exclude sensitive property names viaspring.security.enabledandendpoint.env.show-valuesconfiguration.
Common Pitfalls
- Forgetting getters and setters: Spring Boot’s binder uses reflection to call setters. Without them, binding silently does nothing for mutable fields.
- Mismatched property names with underscores:
APP_MAILING_FROM_ADDRESSin an environment variable binds toapp.mailing.from-address, notapp.mailing.fromAddress. Understand the relaxed binding rules before debugging. - Deep merging behavior: Profile-specific YAML does not replace entire parent objects—it deep-merges. If
app.mailingexists in the base and you override onlyapp.mailing.enabledin the profile, the rest ofapp.mailingis inherited. This is usually what you want, but can be surprising. - Configuration properties not updating at runtime:
@ConfigurationPropertiesbinds once at startup. If you need dynamic updates without restart, consider Spring Cloud Config with a refresh scope or@RefreshScope.
Interview Questions
Spring Boot's relaxed binding normalizes property names from multiple formats before matching them to @ConfigurationProperties fields. A YAML key like app.mailing.from-address can bind to a Java field fromAddress because Spring strips dashes and converts the remaining segments to camelCase. Environment variables follow a similar rule—APP_MAILING_FROM_ADDRESS maps to the same property because underscores act as separators. This means you can use whichever naming convention fits your deployment context (Kubernetes ConfigMaps use kebab-case, shell scripts use SCREAMING_SNAKE_CASE) without changing your Java code.
@ConfigurationProperties binds a group of related properties to a typed Java object with validation support, relaxed binding, and IDE autocomplete via metadata generation. @Value injects individual literal values and supports SpEL expressions but offers none of those benefits. Use @ConfigurationProperties for structured, production configuration that may grow or need validation. Use @Value for quick prototyping, single isolated values, or when you need SpEL. The maintenance and testability advantages of @ConfigurationProperties make it the default choice for anything beyond throwaway code.
Add the @Validated annotation to your @ConfigurationProperties class and apply JSR-380 constraints like @NotBlank, @Min, @Max, and @Email to the fields. Spring Boot's configuration binding checks these constraints during the binding phase. If any constraint fails, the application halts with a BindValidationException and a clear error message listing which property violated which constraint. This catches misconfiguration before the app serves any traffic, which is far better than discovering a wrong timeout value at 3 AM.
Spring Boot silently ignores it. The binder only maps properties that have a corresponding field in the target class. There is no warning or error for extra properties, which can lead to typos in YAML sitting unnoticed for months. Enabling DEBUG logging for org.springframework.boot.context.properties makes the binding visible and helps identify orphaned properties. Alternatively, consider using a schema validator for your YAML files to catch unknown keys as part of your CI pipeline.
Use ApplicationContextRunner with EnableConfigurationProperties to create a minimal context containing only the properties class you want to test. Call withPropertyValues() to inject specific YAML keys, then assert on the bound bean. This approach spins up in milliseconds compared to a full application context, gives precise control over which properties are set, and works well in JUnit 5 tests. It is the standard pattern for unit testing configuration binding without the overhead of a full Spring Boot application.
Further Reading
- Spring Boot Externalized Configuration Reference
- Configuration Metadata — generating property hints for IDEs
- @ConfigurationProperties and @EnableConfigurationProperties
Conclusion
Spring Boot’s @ConfigurationProperties is the recommended way to externalize configuration in production applications. It gives you type-safe, validated, IDE-autocompleteable configuration that adapts across environments through relaxed binding — kebab-case YAML keys map to camelCase Java fields, and environment variables in SCREAMING_SNAKE_CASE connect seamlessly without code changes.
The key to using @ConfigurationProperties well is registration: add @Component or @EnableConfigurationProperties to make the binding class a Spring bean. Use @Validated to catch misconfiguration at startup rather than on the first request. Prefer constructor injection for testability. Profile-specific YAML files deep-merge with the base configuration, letting you override only what changes per environment.
The ApplicationContextRunner test pattern lets you verify binding behavior in milliseconds without loading the full application context, catching configuration errors in unit tests before they surface in production.
Category
Related Posts
Spring Boot Build Tools: Maven & Gradle
Configure Maven and Gradle for Spring Boot projects—plugins, dependency management, packaging JARs and WARs, and build automation essentials.
Embedded Web Servers in Spring Boot: Tomcat, Jetty, Undertow
Configure embedded servers in Spring Boot: compare Tomcat, Jetty, and Undertow, tune thread pools, enable access logs, and switch implementations.
JUnit 5 & Jupiter: Lifecycle, Nested & Parameterized Tests
Explore JUnit 5 Jupiter features: master test lifecycle annotations, organize tests with @Nested, and parameterize tests with @CsvSource and @MethodSource.