Configuration, Properties & Profiles
application.yml, property precedence, type-safe @ConfigurationProperties with records and validation, profiles and secrets.
Externalised configuration
Boot reads configuration from many sources and merges them into one Environment. The same jar runs in every environment; only the configuration changes.
# src/main/resources/application.yml
spring:
application:
name: shop
datasource:
url: jdbc:postgresql://localhost:5432/shop
username: shop
server:
port: 8080
payments:
base-url: https://api.payments.example
timeout: 3s
max-retries: 3
supported-currencies: [GBP, EUR, USD]
Precedence (highest wins)
- Command-line arguments —
--server.port=9090 SPRING_APPLICATION_JSON- Java system properties —
-Dserver.port=9090 - OS environment variables —
SERVER_PORT=9090 - Profile-specific files outside the jar, then inside —
application-prod.yml application.yml/.propertiesoutside the jar, then inside@PropertySourceand defaults
Relaxed binding maps payments.max-retries, PAYMENTS_MAXRETRIES, payments.maxRetries to the same property — which is how environment variables in Kubernetes override YAML.
Type-safe properties with records
Prefer @ConfigurationProperties over scattering @Value("${...}"): it’s typed, grouped, validated, documented by IDE metadata, and testable.
@Validated
@ConfigurationProperties("payments")
public record PaymentProperties(
@NotNull URI baseUrl,
@DefaultValue("5s") Duration timeout,
@Min(0) @Max(10) int maxRetries,
@DefaultValue({"GBP"}) Set<Currency> supportedCurrencies) {
}
@SpringBootApplication
@ConfigurationPropertiesScan // registers every @ConfigurationProperties type
public class ShopApplication { ... }
@Service
class PaymentClient {
PaymentClient(PaymentProperties props, RestClient.Builder builder) {
this.client = builder.baseUrl(props.baseUrl().toString()).build();
this.timeout = props.timeout();
}
}
Boot converts strings into Duration (3s, 500ms, PT1M), DataSize (10MB), URI, enums, Currency, lists and maps. With @Validated, a bad value fails at startup with a clear message instead of in production at 3 a.m.
@Value — for one-off values
@Value("${feature.new-checkout:false}") boolean newCheckout; // with a default
@Value("#{${limits.per-tier}}") Map<String, Integer> limits; // SpEL
Profiles
A profile is a named set of configuration and beans, activated per environment.
# application.yml — shared defaults
spring:
jpa:
open-in-view: false
---
spring:
config:
activate:
on-profile: dev
datasource:
url: jdbc:h2:mem:shop
logging:
level:
org.hibernate.SQL: debug
---
spring:
config:
activate:
on-profile: prod
server:
shutdown: graceful
Activate with SPRING_PROFILES_ACTIVE=prod, --spring.profiles.active=prod, or in tests with @ActiveProfiles("test"). Beans can be profile-specific:
@Configuration
class StorageConfig {
@Bean @Profile("!prod") FileStore localStore() { return new LocalFileStore(Path.of("/tmp/uploads")); }
@Bean @Profile("prod") FileStore s3Store(S3Client s3) { return new S3FileStore(s3, "shop-uploads"); }
}
Profile groups bundle several: spring.profiles.group.prod=prod-db,prod-observability.
Importing more config and secrets
spring:
config:
import:
- optional:file:./config/local.yml # developer overrides, not committed
- optional:configtree:/run/secrets/ # Kubernetes/Docker secrets mounted as files
Never commit secrets. Inject them via environment variables, mounted secret files (configtree:), or a secrets manager integration (Vault, AWS Secrets Manager, Azure Key Vault).
Inspecting configuration
/actuator/env— every property source and value (sanitised)/actuator/configprops— all@ConfigurationPropertiesbeans and their bound values
Mark it complete to track your progress through Spring Boot 4.