Dependency Injection & Bean Lifecycle

Injecting Values, Collections & Properties

18 min Lesson 9 of 13

Injecting Values, Collections & Properties

Constructor and setter injection let you wire beans together. But real applications also need scalar configuration — timeouts, URLs, feature flags, pool sizes — and structured data like lists of allowed origins or maps of locale-to-currency codes. Spring provides two complementary mechanisms for this: the @Value annotation and the type-safe @ConfigurationProperties binding. Knowing when and how to use each one separates clean, maintainable configuration from a tangle of magic strings.

The @Value Annotation

@Value resolves a Spring Expression Language (SpEL) or property-placeholder expression and injects the result into a field, constructor parameter, or setter argument. The most common form uses the ${...} syntax to look up a key in the Environment, which aggregates all property sources (system properties, OS environment variables, application.properties, YAML files, and custom sources) in a well-defined precedence order.

import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component public class PaymentGateway { private final String apiUrl; private final int timeoutMs; private final boolean sandboxMode; // Inject via constructor — recommended: immutable, easy to test public PaymentGateway( @Value("${payment.api.url}") String apiUrl, @Value("${payment.timeout.ms:5000}") int timeoutMs, @Value("${payment.sandbox:false}") boolean sandboxMode) { this.apiUrl = apiUrl; this.timeoutMs = timeoutMs; this.sandboxMode = sandboxMode; } }

The colon syntax — ${key:defaultValue} — supplies a fallback used when the key is absent. Without a default, a missing key throws a BeanCreationException at startup, which is exactly the behavior you want for mandatory settings. Use defaults only for genuinely optional knobs.

The corresponding application.properties:

payment.api.url=https://api.stripe.com/v1 payment.timeout.ms=3000 payment.sandbox=true
Spring's property precedence (highest to lowest): command-line arguments > OS environment variables > application-{profile}.properties > application.properties > @PropertySource files > defaults. This means you can override any property at deployment time without touching the JAR — a key twelve-factor app principle.

SpEL Expressions Inside @Value

The #{...} syntax activates the Spring Expression Language, which goes beyond simple lookups. You can call methods, access other beans, perform arithmetic, and invoke static utilities:

@Value("#{systemProperties['user.home']}") private String userHome; @Value("#{T(java.lang.Runtime).getRuntime().availableProcessors()}") private int cpuCount; // Reference another bean's property @Value("#{dataSource.maxPoolSize * 2}") private int maxBatchSize;

SpEL is powerful, but keep it simple inside @Value. Complex logic belongs in a @Bean factory method or a service class, not in an annotation string that is hard to read and impossible to debug with a breakpoint.

Do not put business logic in SpEL expressions. If an expression grows beyond a single readable line it is a signal to extract it into a proper component. Annotations are not the right home for computation.

Injecting Lists and Arrays

A comma-separated property value is automatically split into a List or array when the injection target is typed accordingly:

# application.properties cors.allowed.origins=https://app.example.com,https://admin.example.com,http://localhost:3000 notification.channels=EMAIL,SMS,PUSH
import java.util.List; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component public class CorsConfig { @Value("${cors.allowed.origins}") private List<String> allowedOrigins; // Spring splits on commas automatically @Value("${notification.channels}") private String[] channels; // also works as a plain array }

Spring's ConversionService handles the split and type coercion. For numeric lists (List<Integer>, int[]) the same technique works — each token is parsed to the target type.

Injecting Maps

Maps require the SpEL form because a flat key=value property cannot carry the map structure on its own:

@Value("#{${locale.currency.map}}") private Map<String, String> localeCurrencyMap;
# application.properties — the value must be a valid SpEL map literal locale.currency.map={en_US:'USD', en_GB:'GBP', ar_SA:'SAR', ja_JP:'JPY'}

The outer #{} evaluates the property value as a SpEL expression, and the ${} inside expands it first. This layering is intentional: ${locale.currency.map} expands to the literal string {en_US:'USD',...}, and then SpEL parses it as a map.

For anything more complex than a handful of keys, prefer @ConfigurationProperties (see below). Map injection via SpEL is fragile — a typo in the property file produces a cryptic parse error at startup rather than a clear validation message.

Loading External Property Files with @PropertySource

Properties do not have to live in application.properties. You can point Spring at any classpath or filesystem resource:

import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; @Configuration @PropertySource("classpath:payment-gateway.properties") @PropertySource(value = "file:/etc/myapp/secrets.properties", ignoreResourceNotFound = true) public class PaymentConfig { // beans that use @Value will now resolve keys from these files too }

ignoreResourceNotFound = true is useful for optional override files that only exist in certain environments (production secrets, developer-specific overrides). Without it, a missing file fails the context load.

Type-Safe Binding with @ConfigurationProperties

When a group of related settings belongs together — a database pool, an email server, a payment gateway — @ConfigurationProperties is the right tool. It binds a prefix of the property namespace to a plain Java object (a POJO), with full type conversion, validation, and IDE auto-completion support.

import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.List; import java.util.Map; @Component @ConfigurationProperties(prefix = "mail") public class MailProperties { private String host; private int port = 587; private String username; private String password; private boolean startTls = true; private List<String> recipients = List.of(); private Map<String, String> templates = Map.of(); // standard getters and setters (Spring uses them 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 boolean isStartTls() { return startTls; } public void setStartTls(boolean s) { this.startTls = s; } public List<String> getRecipients() { return recipients; } public void setRecipients(List<String> r) { this.recipients = r; } public Map<String, String> getTemplates() { return templates; } public void setTemplates(Map<String, String> t) { this.templates = t; } }
# application.properties mail.host=smtp.resend.com mail.port=465 mail.username=resend mail.password=${MAIL_PASSWORD} mail.start-tls=false mail.recipients=ops@example.com,alerts@example.com mail.templates.welcome=templates/welcome.html mail.templates.reset=templates/password-reset.html

Notice mail.start-tls in the properties file maps to setStartTls in the class. Spring's Relaxed Binding normalises kebab-case, camelCase, underscores and uppercase — they are all equivalent for binding purposes. This is why MAIL_PASSWORD (an OS environment variable) can satisfy a property called mail.password.

Add validation: annotate the class with @Validated and use Bean Validation constraints (@NotBlank, @Min, @Max, @Pattern, etc.) on the fields. Spring will refuse to start if the bound values violate the constraints — far better than discovering a misconfiguration at runtime.
import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; 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; @Min(1) private int port = 587; // ... rest of the class }

@Value vs @ConfigurationProperties — The Decision Rule

  • Use @Value for one-off, isolated values — a single timeout, a feature flag, an API key used in exactly one bean. It is quick and requires no extra class.
  • Use @ConfigurationProperties for any cohesive group of three or more related settings. It is self-documenting, validates at startup, and generates IDE metadata that provides auto-complete in application.properties.
  • Never scatter the same prefix across a dozen @Value annotations in different beans. Centralise it in a single @ConfigurationProperties class and inject that class where needed.

Summary

@Value with the ${key:default} syntax is the entry point for scalar and simple collection injection; the #{spel} form unlocks maps and computed values. @PropertySource lets you load additional property files. For groups of related settings, @ConfigurationProperties offers type safety, validation, and relaxed binding — and it should be your default choice for anything that constitutes a configuration namespace. Together, these mechanisms keep configuration out of your code and under the control of the deployment environment.