skip to content

When would you use @Value with SpEL, and what are its limits versus @ConfigurationProperties?

level: seniorimportance: should knowfreq 45%

answer

  1. ${} placeholder + :default
  2. #{} SpEL compute / bean refs
  3. placeholder resolves before SpEL
  4. no relaxed binding, no groups, no @Validated
  5. not on constructor-bound params

basics

~10 s

Use @Value for a single value, optionally computed with SpEL (#{...}) or defaulted (${prop:default}). Its limits: no relaxed binding, no nested/collection binding, no built-in validation, and it resolves eagerly per injection point.

solid answer

~40 s

@Value injects one value into a field, parameter or method. It supports two syntaxes: property placeholders ${my.prop} (with an optional default ${my.prop:fallback}) and SpEL expressions #{...} for computation, e.g. #{systemProperties['user.region'] ?: 'US'} or #{T(java.time.Duration).ofSeconds(30)}. You can nest a placeholder inside SpEL: #{'${app.name}'.toUpperCase()}. Reach for @Value when you need a single, possibly-computed value and a full config class is overkill. Its limits versus @ConfigurationProperties: no relaxed binding (the placeholder must match exactly), no binding of nested objects, lists or maps as a group, no JSR-303 @Validated support, no participation in configuration metadata, and each injection point is independent so the same setting can drift. For any cohesive group of settings, prefer @ConfigurationProperties. Note @Value can't be used on constructor-bound @ConfigurationProperties parameters.

code

java · 14 lines
java
@Component
class ReportService {
    @Value("${report.dir:/var/reports}")           // placeholder + default
    private String dir;

    @Value("#{T(java.time.Duration).ofSeconds(45)}") // SpEL literal
    private Duration timeout;

    @Value("#{'${report.recipients}'.split(',')}")   // placeholder inside SpEL
    private String[] recipients;

    @Value("#{systemEnvironment['HOSTNAME'] ?: 'local'}") // env + Elvis default
    private String host;
}

go deeper

for a junior

Know ${...} injects a value and :default provides a fallback.

for a middle

Distinguish ${} placeholder from #{} SpEL and list the main limits vs @ConfigurationProperties.

for a senior

Explain resolution order, appropriate use cases, and why groups belong in @ConfigurationProperties.

for a principal

Set guidance limiting SpEL in @Value to keep config testable; prefer typed config for anything cohesive or validated.

## The two @Value syntaxes `@Value` (`org.springframework.beans.factory.annotation.Value`) accepts a String that Spring resolves at injection time: 1. **Property placeholder** — `${...}` resolved from the `Environment`: - `@Value("${server.port}")` - With a default: `@Value("${app.timeout:30}")` (used if the property is absent). 2. **SpEL (Spring Expression Language)** — `#{...}` evaluated as an expression: - `@Value("#{systemProperties['user.timezone']}")` - `@Value("#{T(java.lang.Math).random() * 100}")` - Bean references: `@Value("#{someBean.someProperty}")` ## Combining them A placeholder can be embedded *inside* a SpEL literal so you compute over a property value: ```java @Value("#{'${app.name}'.toUpperCase()}") private String upperName; @Value("#{'${app.tags}'.split(',')}") private String[] tags; // manual split; not real relaxed list binding ``` Order matters: placeholders (`${}`) are resolved *before* SpEL (`#{}`) evaluates. ## Where @Value can be applied Fields, setter/constructor parameters (of ordinary beans), and `@Bean` method parameters. It is resolved **eagerly** at injection. ## Limits vs @ConfigurationProperties | Concern | @Value | @ConfigurationProperties | |---|---|---| | Relaxed binding | No — exact name | Yes | | Nested objects / list / map as a group | No | Yes | | JSR-303 validation (@Validated) | No (native) | Yes | | Config metadata / IDE hints | No | Yes | | Grouping / cohesion | Scattered per injection point | One typed bean | | SpEL computation | Yes | No | ## Gotchas - **No relaxed binding**: `@Value("${app.firstName}")` will not match a property written `app.first-name`. You must reference the exact key. - Referencing a missing property with no default throws at startup (`IllegalArgumentException` / placeholder resolution failure). - SpEL over-use makes config hard to test and reason about; keep expressions trivial. - `@Value` is **not** supported on **constructor-bound** `@ConfigurationProperties` parameters. - Splitting a comma list via SpEL is manual and lacks the type conversion/relaxed handling of `@ConfigurationProperties` list binding. ## When to use which - **@Value**: one standalone value, a small computed/default, or a bean-reference expression. - **@ConfigurationProperties**: any cohesive set of settings, anything needing validation, nesting, relaxed binding, or metadata.

  • How do you give @Value a default when the property is missing?
    Use the colon syntax inside the placeholder: @Value("${app.timeout:30}") uses 30 if app.timeout is not defined.
  • What resolves first in @Value("#{'${app.name}'.trim()}") — the placeholder or the SpEL?
    The ${...} placeholder is resolved first, then the resulting string is evaluated by the #{...} SpEL expression.
  • Why won't @Value("${app.first-name}") pick up a value set as app.firstName?
    @Value has no relaxed binding; the placeholder key must match exactly. Only @ConfigurationProperties treats those spellings as equivalent.

saying these in an interview costs you the question

  • Believing @Value supports relaxed binding across kebab/camel forms
  • Thinking @Value can bind a nested config object
  • Assuming SpEL and placeholder are the same syntax
  • Using @Value on constructor-bound @ConfigurationProperties parameters

context