Spring Boot
Externalized Configuration
Externalized Configuration is Spring Boot's mechanism for separating configuration values from application code. Using @Value, @ConfigurationProperties, environment variables, command-line arguments, and Spring's property source hierarchy, you can run the same application binary in development, staging, and production without rebuilding — just swap the config.
Why Externalize Configuration
Externalized configuration is one of the Twelve-Factor App principles: config that varies between deployments (database URLs, API keys, feature flags) must live outside the code, not hardcoded or committed to version control.
Spring Boot supports multiple configuration sources, evaluated in priority order: command-line arguments override environment variables, which override profile-specific application files, which override the base application.properties/yml. This layered system means you can set a sensible default in code and override it at any level without touching the JAR.
The core goal: the same artifact (JAR/Docker image) runs identically in dev, staging, and production — only the externalized config changes.
Property Source Priority Order
Spring Boot evaluates configuration sources from highest to lowest priority. Higher-priority sources override lower ones for the same key.
Shell
# Priority order (1 = highest, overrides all below):
# 1. Command-line arguments
java -jar myapp.jar --server.port=9090 --spring.profiles.active=prod
# 2. @TestPropertySource (tests only)
# 3. @SpringBootTest properties attribute (tests only)
# 4. OS environment variables
export SPRING_DATASOURCE_URL=jdbc:mysql://prod-db:3306/mydb
export SERVER_PORT=8080
# 5. Java system properties (-D flags)
java -Dserver.port=8080 -Dspring.profiles.active=prod -jar myapp.jar
# 6. JNDI attributes (java:comp/env)
# 7. ServletContext / ServletConfig init params
# 8. Random value property source (random.*)
app.secret=${random.uuid}
app.number=${random.int}
app.bounded=${random.int[1, 100]}
# 9. Application properties outside the JAR
# (file:./config/application.properties, file:./application.properties)
# 10. Application properties inside the JAR
# (classpath:/config/application.properties, classpath:/application.properties)
# 11. @PropertySource annotations on @Configuration classes
@Configuration
@PropertySource("classpath:custom.properties")
public class AppConfig { }
# 12. Default properties (SpringApplication.setDefaultProperties)
# Practical implication:
# Set defaults in application.yml (priority 10).
# Override per-environment via env vars (priority 4) in Docker/K8s.
# Override for a single run via --flags (priority 1) during debugging.@Value — Injecting Individual Properties
@Value is the simplest way to inject a single property into a bean. It uses Spring Expression Language (SpEL) syntax and supports default values, type conversion, and expressions.
Java
@Component
public class AppConfig {
// Basic injection — fails with BeanCreationException if key is missing:
@Value("${app.name}")
private String appName;
// With default value — uses "MyApp" if app.title is not set:
@Value("${app.title:MyApp}")
private String title;
// Type conversion — Spring converts the string "8080" to int automatically:
@Value("${server.port:8080}")
private int serverPort;
// Boolean with default:
@Value("${feature.payments.enabled:false}")
private boolean paymentsEnabled;
// List injection — comma-separated value in properties:
// app.allowed-origins=https://myapp.com,https://admin.myapp.com
@Value("${app.allowed-origins}")
private List<String> allowedOrigins;
// SpEL expression — evaluate an expression at inject time:
@Value("#{'${app.allowed-origins}'.split(',')}")
private String[] originsArray;
// Environment variable with fallback:
@Value("${DATABASE_URL:${spring.datasource.url}}")
private String dbUrl;
// Random values (re-evaluated each context load):
@Value("${random.uuid}")
private String instanceId;
}
# application.yml:
app:
name: MyApplication
title: My Spring App
allowed-origins: https://myapp.com,https://admin.myapp.com
# Limitations of @Value:
# - No validation (no @NotNull, @Min, etc.)
# - No IDE auto-completion for property names
# - Scattered across beans — hard to see all config in one place
# - No type-safe binding for nested structures
# Use @ConfigurationProperties for anything beyond simple scalar values.@ConfigurationProperties — Type-Safe Binding
@ConfigurationProperties binds an entire prefix of properties to a Java class. This is the recommended approach for non-trivial configuration: it provides type safety, validation, IDE auto-completion (with the annotation processor), and a single place to see all related config.
Java
// 1. Define a @ConfigurationProperties class:
@ConfigurationProperties(prefix = "app")
@Validated // enables Bean Validation on bound properties
public class AppProperties {
@NotBlank
private String name;
private String title = "My App"; // default value
@Min(1) @Max(65535)
private int port = 8080;
private List<String> allowedOrigins = new ArrayList<>();
private final Jwt jwt = new Jwt();
private final Mail mail = new Mail();
private final Features features = new Features();
// Nested class — bound to app.jwt.*:
public static class Jwt {
@NotBlank
private String secret;
private long expiration = 86400;
private String issuer;
// getters + setters
}
// Nested class with map — bound to app.mail.*:
public static class Mail {
private String from;
private Map<String, String> templates = new HashMap<>();
// getters + setters
}
// Nested class with boolean flags — bound to app.features.*:
public static class Features {
private boolean payments = false;
private boolean notifications = true;
// getters + setters
}
// getters + setters for all fields (or use records in Java 16+)
}
// 2. Register with @EnableConfigurationProperties or @ConfigurationPropertiesScan:
@SpringBootApplication
@ConfigurationPropertiesScan // scans for all @ConfigurationProperties beans
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
// 3. Inject and use — just inject AppProperties anywhere:
@Service
@RequiredArgsConstructor
public class AuthService {
private final AppProperties appProperties;
public String generateToken(String subject) {
return Jwts.builder()
.setSubject(subject)
.setIssuer(appProperties.getJwt().getIssuer())
.setExpiration(new Date(System.currentTimeMillis()
+ appProperties.getJwt().getExpiration() * 1000))
.signWith(Keys.hmacShaKeyFor(
appProperties.getJwt().getSecret().getBytes()))
.compact();
}
}Binding YAML to @ConfigurationProperties
The YAML that backs the AppProperties class above — showing how nested classes, lists, maps, and defaults map to YAML structure.
yaml
# application.yml — binds to AppProperties (prefix = "app"):
app:
name: MyApplication
title: My Spring App
port: 8080
allowed-origins:
- https://myapp.com
- https://admin.myapp.com
- http://localhost:3000
jwt:
secret: ${JWT_SECRET}
expiration: 86400
issuer: https://myapp.com
mail:
from: noreply@myapp.com
templates:
welcome: classpath:templates/welcome.html
reset: classpath:templates/password-reset.html
features:
payments: true
notifications: true
# Spring Boot's relaxed binding rules:
# app.allowedOrigins = app.allowed-origins = APP_ALLOWED_ORIGINS (env var)
# All three bind to the same field — Spring normalizes them automatically.
# Convention: use kebab-case in YAML, UPPER_SNAKE_CASE for env vars.
# Overriding nested properties with environment variables (Docker/K8s):
# app.jwt.secret → APP_JWT_SECRET
# app.mail.from → APP_MAIL_FROM
# app.features.payments.enabled → APP_FEATURES_PAYMENTS_ENABLEDEnvironment Variables and Relaxed Binding
Spring Boot's relaxed binding maps environment variables to property keys automatically. This is the primary mechanism for injecting secrets and environment-specific config in Docker and Kubernetes deployments.
yaml
# Spring Boot relaxed binding — all four forms bind to the same property:
# spring.datasource.url (YAML/properties canonical form)
# spring.datasource.url (property key)
# spring.datasource-url (not standard — avoid)
# SPRING_DATASOURCE_URL (OS environment variable)
# spring.datasource.url (Java system property)
# ── Docker — pass env vars to the container ───────────────────────────
docker run \
-e SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/prod \
-e SPRING_DATASOURCE_USERNAME=appuser \
-e SPRING_DATASOURCE_PASSWORD=${DB_SECRET} \
-e SPRING_PROFILES_ACTIVE=prod \
-e APP_JWT_SECRET=${JWT_SECRET} \
myapp:latest
# ── Docker Compose ─────────────────────────────────────────────────────
services:
app:
image: myapp:latest
environment:
SPRING_PROFILES_ACTIVE: prod
SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/mydb
SPRING_DATASOURCE_USERNAME: appuser
SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}
APP_JWT_SECRET: ${JWT_SECRET}
env_file:
- .env.prod # load from file — keep secrets out of compose.yml
# ── Kubernetes — ConfigMap (non-sensitive config) ──────────────────────
apiVersion: v1
kind: ConfigMap
metadata:
name: myapp-config
data:
SPRING_PROFILES_ACTIVE: "prod"
SPRING_DATASOURCE_URL: "jdbc:mysql://db-service:3306/mydb"
APP_MAIL_FROM: "noreply@myapp.com"
# ── Kubernetes — Secret (sensitive values) ─────────────────────────────
apiVersion: v1
kind: Secret
metadata:
name: myapp-secrets
type: Opaque
stringData:
SPRING_DATASOURCE_PASSWORD: "supersecret"
APP_JWT_SECRET: "jwt-signing-key"
# Reference in deployment:
envFrom:
- configMapRef:
name: myapp-config
- secretRef:
name: myapp-secretsConfiguration Validation with @Validated
Adding @Validated to @ConfigurationProperties enables Bean Validation (JSR-380). Spring Boot validates all @ConfigurationProperties beans at startup — misconfiguration fails fast with a descriptive error rather than at runtime.
Java
@ConfigurationProperties(prefix = "app")
@Validated
public class AppProperties {
@NotBlank(message = "app.name must not be blank")
private String name;
@Min(value = 1024, message = "app.port must be >= 1024")
@Max(value = 65535, message = "app.port must be <= 65535")
private int port;
@Valid // cascade validation into nested objects
@NotNull
private final Jwt jwt = new Jwt();
@Valid
@NotNull
private final Database database = new Database();
public static class Jwt {
@NotBlank(message = "app.jwt.secret is required")
@Size(min = 32, message = "app.jwt.secret must be at least 32 characters")
private String secret;
@Positive(message = "app.jwt.expiration must be positive")
private long expiration = 86400;
@NotBlank
@Pattern(regexp = "https?://.+", message = "app.jwt.issuer must be a valid URL")
private String issuer;
// getters + setters
}
public static class Database {
@NotBlank
private String url;
@Min(1) @Max(100)
private int maxPoolSize = 10;
// getters + setters
}
}
// If validation fails at startup, Spring Boot throws:
// ***************************
// APPLICATION FAILED TO START
// ***************************
// Description:
// Binding to target AppProperties failed:
// Property: app.jwt.secret
// Value: ""
// Reason: app.jwt.secret is required
//
// This fails loudly at boot — not silently at 3am when the first request hits.application.properties Metadata (IDE Auto-Completion)
Adding the Spring Boot Configuration Processor annotation processor generates additional-spring-configuration-metadata.json. This file powers IDE auto-completion, documentation, and type hints for your custom @ConfigurationProperties keys.
XML
<!-- pom.xml — add the annotation processor: -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
# After adding, rebuild and your custom properties get:
# - IDE auto-completion in application.yml / application.properties
# - Type information (int, boolean, List<String>)
# - Javadoc-sourced descriptions in the IDE tooltip
# - Deprecation warnings if you annotate with @DeprecatedConfigurationProperty
# You can also write manual hints in:
# src/main/resources/META-INF/additional-spring-configuration-metadata.json
{
"properties": [
{
"name": "app.features.payments",
"type": "java.lang.Boolean",
"description": "Enable the payments feature module. Requires Stripe credentials.",
"defaultValue": false
},
{
"name": "app.jwt.expiration",
"type": "java.lang.Long",
"description": "JWT token expiration time in seconds.",
"defaultValue": 86400
}
]
}