Skip to content
ExponaBLOG
JAVA, SPRING FRAMEWORK & SPRING BOOT · DAY 027 / 100

@ConfigurationProperties for typed config for production

Stop sprinkling @Value strings across your code.

Expona Admin5 min read

The problem with scattered @Value annotations

When you start a Spring application, it's tempting to sprinkle @Value annotations throughout your codebase to inject configuration strings. A database URL here, an API timeout there, a mail server address in another service. Six months later, you have configuration scattered across dozens of classes, with no type safety, no validation, and no way to know if your production config is even valid until runtime—or worse, in production.

@ConfigurationProperties for typed config for production is a fundamentally different approach. Instead of decorating individual fields, you bind an entire configuration prefix to a strongly-typed class (often a record), validate it at startup, and inject one cohesive object wherever you need it. Your IDE knows the shape of your config, the application fails fast if required fields are missing, and your team can reason about configuration as a contract, not a pile of strings.

Bind a prefix to a record

Let's start with the simplest pattern. You have a properties file with mail settings:

java
mail.host=smtp.example.com
mail.port=587
mail.username=noreply@example.com
mail.password=secret123

Instead of injecting each one individually, create a record with the @ConfigurationProperties annotation:

java
package com.example.config;

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

@ConfigurationProperties("mail")
public record MailProps(
    String host,
    int port,
    String username,
    String password
) {}

The prefix "mail" tells Spring to map any property starting with mail. to the corresponding field. Spring uses the constructor parameters to bind values, matching them by name. Register it in your configuration:

java
package com.example.config;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties(MailProps.class)
public class AppConfig {}

Now inject it anywhere:

java
@Service
public class MailService {
    private final MailProps mailProps;

    public MailService(MailProps mailProps) {
        this.mailProps = mailProps;
    }

    public void sendEmail(String to, String subject, String body) {
        // Use mailProps.host(), mailProps.port(), etc.
        // All strongly typed, never a string cast
    }
}

That's it. One object, fully typed, reusable across your application. No hunting through the codebase to find where mail configuration lives.

Validate with @Validated and constraints

Binding configuration is useful, but validation is what prevents your application from deploying with a broken config. Use Jakarta validation annotations (or javax.validation if on older Spring Boot) combined with @Validated:

java
package com.example.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;

@ConfigurationProperties("mail")
@Validated
public record MailProps(
    @NotBlank(message = "mail.host must not be blank")
    String host,

    @Min(value = 1, message = "mail.port must be between 1 and 65535")
    int port,

    @NotBlank
    String username,

    @NotBlank
    String password,

    @Pattern(regexp = "^[A-Z]+$", message = "mail.protocol must be uppercase")
    String protocol
) {}

Now add this to your properties file with invalid values:

java
mail.host=
mail.port=0
mail.username=admin
mail.password=secret
mail.protocol=smtp

Start the application. Spring will validate the configuration against your constraints and throw a ConfigurationPropertiesBindException during startup, listing all validation errors. Your team sees the problem immediately, not after production traffic reveals it.

Fail fast at startup on bad config

The magic of this pattern is timing. Spring validates configuration properties during the application startup phase, before any beans are fully constructed. If validation fails, the entire context fails to load. This is exactly what you want in production.

Consider a real scenario: you're deploying to a new environment, and DevOps accidentally leaves out the mail.host property. With scattered @Value annotations, the application starts fine, and the first time someone tries to send an email, a NullPointerException crashes the request. With @ConfigurationProperties and validation, the pod never comes up. Your deployment pipeline catches it, you get an alert, and you fix the config before traffic hits it.

You can also add custom validation if your constraints are more complex:

java
@ConfigurationProperties("mail")
@Validated
public record MailProps(
    @NotBlank String host,
    @Min(1) int port,
    String username,
    String password
) {
    @jakarta.validation.constraints.AssertTrue(
        message = "If username is set, password must also be set"
    )
    private boolean isAuthValid() {
        return (username == null && password == null) ||
               (username != null && password != null);
    }
}

Custom validation methods let you enforce business rules at startup. No surprises at 3 AM.

IDE autocompletion via the metadata processor

One of the quieter benefits of @ConfigurationProperties is IDE support. Spring provides a compile-time annotation processor that generates metadata about your configuration classes. Your IDE uses this metadata to offer autocompletion when you're editing application.properties or application.yml.

Add the dependency (it's usually included automatically in Spring Boot projects):

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

Compile your project. The processor generates META-INF/spring-configuration-metadata.json. Now when you open application.properties, your IDE knows about mail.host, mail.port, and their types:

Feature Benefit
Autocomplete Type property names without hunting documentation
Validation in the IDE Red squiggly line if you typo a property name
Inline docs Hover over a property to see its description
Quick navigation Jump from property file to Java definition

This turns configuration files into a first-class experience. Your team can't accidentally misspell mail.hoost because the IDE stops them.

Common mistakes

  • Forgetting @EnableConfigurationProperties. If you define the record but don't enable it, Spring won't bind anything. Always register your properties classes in a @Configuration class, or use @ConfigurationPropertiesScan on your main class to scan a package.
  • Mixing kebab-case and camelCase. Spring normalizes property names, so mail.max-retries in the file binds to maxRetries in the record. Be consistent: use kebab-case in properties files and camelCase in Java. Spring handles the mapping.
  • Ignoring nested objects. If you have complex configuration, use nested records. Spring binds hierarchical properties just as easily as flat ones.
  • Treating validation errors as optional. Some teams catch ConfigurationPropertiesBindException and log it. Don't. Let it fail. That's the whole point.
  • Not documenting properties. Use the @Value parameter in the processor annotation or Javadoc comments. Your future self and your team will thank you.

Key takeaways

  • @ConfigurationProperties binds a prefix to a typed class (record), eliminating scattered @Value strings and giving your code a single, clear configuration contract.
  • Combine it with @Validated and Jakarta validation annotations to enforce constraints at startup, catching misconfigurations before they reach production.
  • Spring's annotation processor generates metadata that powers IDE autocompletion and validation in your properties files, preventing typos and making configuration discoverable.
  • Use custom validation methods for complex rules that involve multiple fields, ensuring your configuration is not just present but sensible.
  • Always enable your properties classes and let validation failures crash startup. Fast failure is a feature, not a bug.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles