@ConfigurationProperties vs @Value + relaxed binding

java dev.to

Every application needs to change its behaviour without changing its code. The database URL is different on your laptop than in production. The timeout you want in a test is not the one you want live. This is externalized configuration: the values that live outside the compiled code, in a file or an environment variable, so you can turn knobs without a rebuild.

Spring Boot reads all of those sources — application.properties, application.yml, environment variables, command-line arguments — and merges them into one big lookup table it calls the Environment. The question this article answers is the next one: once a value is sitting in that table, how do you get it into your code? Spring gives you two ways, and knowing when to reach for each is the whole game.

The simplest way in: @Value

Say your application.properties has one line:

```plain text
app.greeting=Hello there





You want that string inside a bean. The most direct tool is the **`@Value`** annotation. You put it on a field, and inside it you write a **placeholder** — the property's key wrapped in `${...}`:




```java
@Component
class GreetingService {

    @Value("${app.greeting}")
    private String greeting;
}
Enter fullscreen mode Exit fullscreen mode

When Spring builds this bean, it sees the placeholder, looks up app.greeting in the Environment, finds Hello there, and drops that string into the field. That is the entire mechanism. One annotation, one property, one field.

You can hand it a fallback in case the property is missing. Put a colon after the key and write the default:

@Value("${app.greeting:Hi}")
private String greeting;
Enter fullscreen mode Exit fullscreen mode

Now if nobody ever sets app.greeting, the field holds Hi instead of blowing up. Without that default, a missing property makes the whole application fail to start — which is sometimes exactly what you want.

@Value can also do arithmetic and logic through SpEL (the Spring Expression Language), written with #{...} instead of ${...}. That is a separate feature and mostly a distraction here, so we will set it aside and stay with plain property injection.

Where @Value starts to hurt

One value is fine. The trouble starts when a feature needs many related values. Imagine configuring an outbound mail client:

```plain text
app.mail.host=smtp.example.com
app.mail.port=587
app.mail.username=mailer
app.mail.from=noreply@example.com





With `@Value` you inject them one field at a time:




```java
@Component
class MailClient {

    @Value("${app.mail.host}")
    private String host;

    @Value("${app.mail.port}")
    private int port;

    @Value("${app.mail.username}")
    private String username;

    @Value("${app.mail.from}")
    private String from;
}
Enter fullscreen mode Exit fullscreen mode

Look at what just happened. Four properties that clearly belong together are scattered across four annotations. The shared app.mail prefix is repeated four times. Nothing groups them into a single idea. And every class that needs mail settings copies this whole ritual again.

There is a subtler problem too. If you fat-finger the key as ${app.mail.hostt}, nothing warns you — the field just fails to resolve at startup, one property at a time. There is no single object you can point at and say "these are the mail settings." The configuration has no shape.

That missing shape is the gap the second tool fills.

The typed way in: @ConfigurationProperties

Instead of pulling values one by one, what if you described the group once, as a plain Java class, and asked Spring to fill the whole thing in? That is exactly what @ConfigurationProperties does. It binds a chunk of the Environment onto the fields of an object, matched by a shared prefix.

@ConfigurationProperties(prefix = "app.mail")
class MailProperties {

    private String host;
    private int port;
    private String username;
    private String from;

    // getters and setters
}
Enter fullscreen mode Exit fullscreen mode

The prefix = "app.mail" says: take every property under app.mail, strip that prefix, and match what is left against my field names. So app.mail.host fills host, app.mail.port fills port, and so on. This kind of object — a typed holder for a group of related settings — is usually called a properties class.

One thing must click here: the prefix appears once. Add a fifth setting later and you add one field, not another scattered annotation. The mail configuration now has a name and a type you can pass around:

@Component
class MailClient {

    private final MailProperties props;

    MailClient(MailProperties props) {
        this.props = props;
    }

    void send() {
        connect(props.getHost(), props.getPort());
    }
}
Enter fullscreen mode Exit fullscreen mode

Turning it on

A properties class does nothing until Spring knows to bind and register it. There are a couple of ways, and you only need one.

The common approach is to name the classes at startup with @EnableConfigurationProperties, usually on your main application class:

@SpringBootApplication
@EnableConfigurationProperties(MailProperties.class)
class Application { }
Enter fullscreen mode Exit fullscreen mode

That line tells Spring: create a MailProperties bean, bind the app.mail.* values into it, and make it available for injection. From then on MailProperties is an ordinary bean you can inject anywhere. (You can instead put @ConfigurationPropertiesScan on the application to auto-discover every properties class, so you never list them by hand.)

Relaxed binding: many spellings, one field

Here is where the two tools truly part ways, and it is the heart of this topic.

@Value matches keys exactly. ${app.mail.host} finds app.mail.host and nothing else. But configuration arrives from wildly different places with different naming customs. A YAML file likes app.mail.from-address. An operating system environment variable must be APP_MAIL_FROMADDRESS, because shells do not allow dots or dashes. These are the same setting wearing different clothes.

@ConfigurationProperties understands this. It uses relaxed binding: a single field can be filled by any of several equivalent spellings of its name. For a field called fromAddress, all of these bind to it:

```plain text
app.mail.from-address=noreply@example.com # kebab-case
app.mail.fromAddress=noreply@example.com # camelCase
app.mail.from_address=noreply@example.com # underscores
APP_MAIL_FROMADDRESS=noreply@example.com # environment variable





Spring normalizes both the incoming key and your field name to a canonical form before comparing, so the punctuation and casing stop mattering. This is why the same code runs unchanged on your laptop (reading a `.yml` file) and in a container (reading environment variables). You do not write four variants; you write one field and let relaxed binding absorb the rest.


The recommended style in your own files is **kebab-case** — `from-address` — because it is the one canonical form the documentation uses. But the point of relaxed binding is that you are never trapped by that choice.


`@Value` gets none of this. With `@Value("${app.mail.from-address}")` you must match the key character for character, which is exactly why it feels brittle the moment your properties come from an environment variable.


## Types come for free


Because a properties class has real fields with real types, Spring converts strings into those types for you. `port` above is an `int`, and Spring parses `"587"` into the number `587` without you asking.


This goes well beyond numbers. Spring can turn text into rich types out of the box:




```java
@ConfigurationProperties(prefix = "app.mail")
class MailProperties {

    private Duration timeout;          // "5s"  -> 5 seconds
    private DataSize maxAttachment;    // "10MB" -> 10 megabytes
    private List<String> bccList;      // comma-separated -> a List
}
Enter fullscreen mode Exit fullscreen mode

```plain text
app.mail.timeout=5s
app.mail.max-attachment=10MB
app.mail.bcc-list=ops@example.com,alerts@example.com





**`Duration`** and **`DataSize`** are Spring's types for "an amount of time" and "an amount of data." Instead of writing milliseconds and byte counts, you write `5s` and `10MB` and Spring parses the units. A `List<String>` splits a comma-separated value automatically. Doing any of this through `@Value` means writing conversion logic by hand.


## Nesting groups inside groups


Real configuration has structure, and properties classes mirror it. A field whose type is another properties-style class becomes a **nested** group:




```java
@ConfigurationProperties(prefix = "app.mail")
class MailProperties {

    private String host;
    private final Retry retry = new Retry();

    static class Retry {
        private int maxAttempts;
        private Duration backoff;
        // getters and setters
    }
    // getters and setters
}
Enter fullscreen mode Exit fullscreen mode

The nested object extends the prefix path. retry under the app.mail prefix means its fields live under app.mail.retry:

```plain text
app.mail.retry.max-attempts=3
app.mail.retry.backoff=2s





Now the tree of properties matches the tree of objects, and related knobs stay grouped instead of flattening into a soup of dotted keys.


## Fail fast with validation


A wrong config value should stop the application at startup, not surface as a mysterious error an hour later under load. Properties classes support **validation** for exactly this.


Put `@Validated` on the class and standard bean-validation annotations on the fields:




```java
@Validated
@ConfigurationProperties(prefix = "app.mail")
class MailProperties {

    @NotBlank
    private String host;

    @Min(1) @Max(65535)
    private int port;
    // getters and setters
}
Enter fullscreen mode Exit fullscreen mode

If host is blank or port is 70000, Spring refuses to start and tells you which property is wrong and why. The bad deployment dies immediately, with a clear message, instead of limping forward. @Value has no equivalent — validating an injected value is on you.

Setters, or a constructor for immutability

By default binding works through setters: Spring makes the object with its no-arg constructor, then calls setHost(...), setPort(...) for each value. That means your fields cannot be final, and the object is mutable after startup.

If you would rather have an immutable settings object, use constructor binding — let Spring pass the values in through the constructor instead:

@ConfigurationProperties(prefix = "app.mail")
class MailProperties {

    private final String host;
    private final int port;

    MailProperties(String host, int port) {
        this.host = host;
        this.port = port;
    }
    // getters only
}
Enter fullscreen mode Exit fullscreen mode

Now the fields are final, there are no setters, and nothing can mutate the config after it is built. A Java record — which is nothing but a bunch of final fields and a constructor — is the natural fit and reads even cleaner:

@ConfigurationProperties(prefix = "app.mail")
record MailProperties(String host, int port, Duration timeout) { }
Enter fullscreen mode Exit fullscreen mode

One caution: a class binds through either setters or a constructor, not a confused mix. If you give it a value-taking constructor, Spring uses constructor binding for the whole object.

So which one do you reach for

The dividing line is simple once you have seen both.

  • Reach for @Value when you need one loose value in one place — a feature flag, a single label, a lone timeout — and you do not need conversion, grouping, or validation. It is the light tool for the light job.
  • Reach for @ConfigurationProperties the moment settings travel in a group, or you want type conversion, nested structure, validation, or relaxed binding across file-and-environment sources. Which, for anything beyond a stray value or two, is almost always.

The through-line is that @Value reads the Environment key by key, while @ConfigurationProperties gives that configuration a shape — a typed, named, validated object that binds itself from whatever spelling your deployment happens to use. Once your configuration has more than a couple of moving parts, that shape is what keeps it honest.

Source: dev.to

arrow_back Back to Tutorials