Definition
- Property resolution order – CLI args (highest), JVM system properties, OS environment variables, profile-specific
application-{profile}.yml, baseapplication.yml,@PropertySource,@Valuedefaults (lowest). - Profiles – activate environment-specific configuration by setting
spring.profiles.active. Profile-specific files overlay the base file: properties not overridden keep their base values. - @ConfigurationProperties – binds a group of related properties to a typed immutable Java record with constructor binding, relaxed name binding (kebab-case to camelCase), and
@Validatedfor startup-time validation. - Config vs code – configuration selects WHICH behavior is active (feature flags, URLs, timeouts), while code defines HOW that behavior works (algorithms, business rules).
- Spring Cloud Config – a centralized configuration server backed by Git that serves properties to all microservices.
Think of different rule books for different National Bank of India branches: dev, staging, and production. The branch manager's direct verbal order (CLI argument) overrides the regional memo (environment variable), which overrides the branch handbook (application.yml). @ConfigurationProperties is a structured form that the manager validates when the branch opens. If any required field is missing, the branch refuses to open rather than discovering the problem during the first customer transaction.
Understanding the Concepts
What is the property resolution order?
Spring Boot resolves properties from multiple sources in a strict priority order. When the same property key exists in more than one source, the highest-priority source wins.
- Command-line arguments (
--server.port=9090) : highest priority - JVM system properties (
-Dserver.port=9090) - OS environment variables (
SERVER_PORT=9090: note the UPPER_SNAKE_CASE mapping) - Profile-specific
application-{profile}.ymlfiles - Base
application.ymlorapplication.properties @PropertySourceannotations on configuration classes- Default values in
@Value("${key:default}"): lowest priority
This order matters in production. Rajesh, the developer at National Bank of India, sets the database URL in application-prod.yml, but the operations team can override it at deploy time with an environment variable without changing any file.
package com.bank.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class InterestService {
// Resolved from the highest-priority source that defines this key
// Default value 0.065 is used only if no source defines it
@Value("${bank.interest-rate:0.065}")
private BigDecimal interestRate;
public BigDecimal calculateInterest(BigDecimal principal, int months) {
return principal.multiply(interestRate)
.multiply(BigDecimal.valueOf(months))
.divide(BigDecimal.valueOf(12), 2, java.math.RoundingMode.HALF_UP);
}
}
# Override at deploy time without changing any file
java -jar national-bank-app.jar --bank.interest-rate=0.075
# Or via environment variable
export BANK_INTEREST_RATE=0.075
java -jar national-bank-app.jar
Resolution order for bank.interest-rate:
1. --bank.interest-rate=0.075 (CLI) <- wins if present
2. -Dbank.interest-rate=0.075 (JVM prop)
3. BANK_INTEREST_RATE=0.075 (env var)
4. application-prod.yml (profile)
5. application.yml (base)
6. @Value default: 0.065 (fallback)
How do profiles work?
Profiles activate environment-specific configuration. You set the active profile, and Spring Boot loads the matching application-{profile}.yml file on top of the base application.yml. Properties defined in the profile-specific file override the base file. Properties not overridden keep their base values.
At National Bank of India, Pankaj defines three profiles: dev for local development with H2 in-memory database and DEBUG logging, staging for the UAT environment with PostgreSQL and INFO logging, and prod for production with PostgreSQL, connection pool tuning, and WARN logging.
# application.yml (base: shared across all environments)
bank:
name: National Bank of India
max-retries: 3
interest-rate: 0.065
server:
port: 8080
# application-dev.yml (dev overrides)
spring:
datasource:
url: jdbc:h2:mem:devdb
driver-class-name: org.h2.Driver
logging:
level:
com.bank: DEBUG
# application-prod.yml (prod overrides)
spring:
datasource:
url: jdbc:postgresql://prod-db:5432/nationalbank
hikari:
maximum-pool-size: 20
minimum-idle: 5
logging:
level:
com.bank: WARN
# Activate a profile
java -jar national-bank-app.jar --spring.profiles.active=prod
# Or via environment variable (common in Kubernetes)
export SPRING_PROFILES_ACTIVE=prod
java -jar national-bank-app.jar
package com.bank.service;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
@Profile("prod")
public class LiveRateProvider implements RateProvider {
@Override
public BigDecimal getRate(String fromCurrency, String toCurrency) {
// Calls external forex API in production
return externalForexClient.fetchRate(fromCurrency, toCurrency);
}
}
@Service
@Profile("dev")
public class MockRateProvider implements RateProvider {
@Override
public BigDecimal getRate(String fromCurrency, String toCurrency) {
// Returns fixed rate in development
return new BigDecimal("83.50");
}
}
How does @ConfigurationProperties work?
@ConfigurationProperties is the recommended way to read configuration in production Spring Boot applications. It binds a group of related properties to a typed Java object with startup-time validation. This is far safer than using @Value for multiple related properties.
Spring Boot uses relaxed binding, which means the property bank.max-retries in YAML maps automatically to the field maxRetries in Java. It also maps bank.interest-rate to interestRate. You do not need to match the exact case.
package com.bank.config;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.time.Duration;
@Validated
@ConfigurationProperties(prefix = "bank")
public record BankConfig(
@NotBlank String name,
@NotNull BigDecimal interestRate,
@Min(1) @Max(10) int maxRetries,
Duration timeout,
ApiConfig api
) {
public record ApiConfig(
@NotBlank String baseUrl,
@NotBlank String apiKey,
Duration connectTimeout
) {}
}
# application-prod.yml
bank:
name: National Bank of India
interest-rate: 0.065
max-retries: 3
timeout: 30s
api:
base-url: https://api.nationalbank.in
api-key: ${BANK_API_KEY} # read from environment variable
connect-timeout: 5s
package com.bank.service;
import com.bank.config.BankConfig;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
@Service
public class LoanService {
private final BankConfig bankConfig;
public LoanService(BankConfig bankConfig) {
this.bankConfig = bankConfig;
}
public BigDecimal calculateEMI(BigDecimal principal, int months) {
BigDecimal rate = bankConfig.interestRate();
BigDecimal monthlyRate = rate.divide(BigDecimal.valueOf(12), 10, RoundingMode.HALF_UP);
// EMI = P * r * (1+r)^n / ((1+r)^n - 1)
BigDecimal onePlusR = BigDecimal.ONE.add(monthlyRate);
BigDecimal power = onePlusR.pow(months);
return principal.multiply(monthlyRate).multiply(power)
.divide(power.subtract(BigDecimal.ONE), 2, RoundingMode.HALF_UP);
}
}
If bank.api.api-key is missing in prod:
-> Application FAILS to start with:
"Binding validation errors on bank: api.apiKey must not be blank"
-> Priya never sees a broken service. The error is caught at deploy time.
How is @ConfigurationProperties different from @Value?
| Feature | @Value | @ConfigurationProperties |
|---|---|---|
| Type safety | String by default, manual conversion | Automatic type binding |
| Validation | None | @Validated with Bean Validation |
| Immutability | Mutable field injection | Immutable Java record |
| Nested properties | One annotation per field | Nested records automatically |
| Relaxed binding | No | Yes (kebab-case to camelCase) |
| IDE support | Limited | Full autocomplete with metadata |
| Testing | Requires Spring context | Inject config object directly |
| Best for | Simple one-off values | Groups of related settings |
// @Value approach: simple but fragile
package com.bank.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class SimpleInterestService {
@Value("${bank.interest-rate:0.065}")
private BigDecimal interestRate;
@Value("${bank.name}")
private String bankName;
// No validation: if bank.name is missing, app starts but
// fails at runtime when this field is used
}
// @ConfigurationProperties approach: type-safe and validated
// See BankConfig record above -- validated at startup
What belongs in config vs what belongs in code?
Pankaj established a clear rule at National Bank of India: configuration selects WHICH behavior is active, and code defines HOW that behavior works.
| Belongs in Config | Belongs in Code |
|---|---|
| Database URL | SQL queries |
| Feature flags (enabled/disabled) | Business logic |
| API keys and secrets | Authentication algorithm |
| Timeout durations | Retry strategy implementation |
| Interest rate value | Interest calculation formula |
| Rate provider selection (mock/live) | Rate conversion algorithm |
# Config SELECTS which strategy is active
bank:
rate-provider: live # switch to "mock" for dev
fraud-check-enabled: true # disable for testing
interest-rate: 0.065 # the VALUE comes from config
package com.bank.service;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
// Config selects WHICH bean is active
@Service
@ConditionalOnProperty(name = "bank.rate-provider", havingValue = "live")
public class LiveRateProvider implements RateProvider {
@Override
public BigDecimal getRate(String from, String to) {
// Code DEFINES how rates are fetched
return externalApiClient.fetchRate(from, to);
}
}
What are common mistakes with configuration?
The most common mistake is using @Value for required production properties without validation. The application starts successfully but fails at runtime when the property is first accessed.
// WRONG: no validation, fails at runtime
package com.bank.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Service
public class PaymentService {
@Value("${bank.payment-gateway.api-key}")
private String apiKey; // If missing -> startup fails with unclear error
@Value("${bank.payment-gateway.url}")
private String gatewayUrl;
@Value("${bank.payment-gateway.timeout}")
private int timeout; // If value is "30s" -> NumberFormatException at runtime
}
// CORRECT: @ConfigurationProperties with validation, fails fast at startup
package com.bank.config;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import java.time.Duration;
@Validated
@ConfigurationProperties(prefix = "bank.payment-gateway")
public record PaymentGatewayConfig(
@NotBlank String apiKey,
@NotBlank String url,
@NotNull Duration timeout // Duration handles "30s" correctly
) {}
Another mistake is hardcoding environment-specific values in the base application.yml. These should be in profile-specific files or environment variables.
How does Spring Cloud Config centralize configuration?
In a microservices architecture with dozens of services, managing configuration files in each service becomes difficult. Spring Cloud Config provides a centralized configuration server backed by a Git repository. Each microservice fetches its configuration from the server at startup.
Git Repository (config-repo)
├── account-service.yml
├── account-service-prod.yml
├── loan-service.yml
├── loan-service-prod.yml
└── application.yml <- shared across all services
┌──────────────┐
│ Config Server│
│ (Git-backed) │
└──────┬───────┘
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│Account Service│ │ Loan Service │ │Payment Service│
└───────────────┘ └───────────────┘ └───────────────┘
# bootstrap.yml in each microservice
spring:
application:
name: account-service
cloud:
config:
uri: http://config-server:8888
What should a senior engineer know?
- Use
spring.config.importto import additional configuration sources, including Spring Cloud Config Server and Vault for secrets management. @ConfigurationPropertiessupports constructor binding by default with Java records, making configuration objects immutable and thread-safe.- Use
spring-boot-configuration-processorto generate metadata so IDEs provide autocomplete for your custom properties. - Secrets should never be stored in
application.ymlfiles or Git repositories. Use Kubernetes secrets, HashiCorp Vault, or AWS Secrets Manager, and reference them via environment variables. - Pankaj uses
@ConfigurationPropertiesScanon the main class to automatically discover all@ConfigurationPropertiesrecords without needing@EnableConfigurationProperties.
package com.bank;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
@SpringBootApplication
@ConfigurationPropertiesScan // auto-discovers all @ConfigurationProperties records
public class NationalBankApplication {
public static void main(String[] args) {
SpringApplication.run(NationalBankApplication.class, args);
}
}
What are common interview follow-up questions?
- Can a profile-specific property file override the base
application.yml? Yes. Profile-specific files have higher priority than the base file. - Can you activate multiple profiles at once? Yes.
spring.profiles.active=prod,metricsactivates both. Properties from later profiles override earlier ones. - How do you set a property only if it is not already defined? Use
@Value("${key:default}")for a default value, ormatchIfMissing = trueon@ConditionalOnProperty. - What is the difference between
application.ymlandapplication.properties? YAML supports hierarchical structure and is more readable for nested config. Properties files use flat dot notation. Both work identically in Spring Boot.
Visual Overview
Property Resolution Order (highest wins):
┌─────────────────────────────────────┐
│ 1. Command-line args │ --server.port=9090
├─────────────────────────────────────┤
│ 2. JVM system properties │ -Dserver.port=9090
├─────────────────────────────────────┤
│ 3. OS environment variables │ SERVER_PORT=9090
├─────────────────────────────────────┤
│ 4. Profile-specific YAML │ application-prod.yml
├─────────────────────────────────────┤
│ 5. Base application.yml │ application.yml
├─────────────────────────────────────┤
│ 6. @PropertySource │ custom source
├─────────────────────────────────────┤
│ 7. @Value defaults │ @Value("${key:default}")
└─────────────────────────────────────┘
Profile Overlay:
┌─────────────────────┐
│ application.yml │ bank.name=National Bank
│ (base) │ bank.max-retries=3
└──────────┬──────────┘ server.port=8080
│
▼ overlays
┌─────────────────────┐
│ application-prod.yml │ spring.datasource.url=...
│ (profile) │ logging.level.com.bank=WARN
└──────────┬──────────┘
│
▼ result
┌─────────────────────┐
│ Merged config │ bank.name=National Bank (from base)
│ │ bank.max-retries=3 (from base)
│ │ server.port=8080 (from base)
│ │ spring.datasource.url=... (from prod)
│ │ logging.level=WARN (from prod)
└─────────────────────┘
Key Takeaway
Spring Boot resolves properties from multiple sources in strict precedence: command-line arguments override JVM properties, which override environment variables, which override profile-specific files, which override the base application.yml. Profile-specific files layer on top of the base, and properties not overridden keep their base values. This layered approach means Rajesh can define sensible defaults in the base file while the operations team overrides database URLs and secrets via environment variables at deploy time without touching code.
@ConfigurationProperties is the production standard for reading configuration. It binds a group of related properties into a typed immutable record with constructor binding, relaxed name binding so cache-ttl maps to cacheTtl, and @Validated for startup-time validation. A missing API key in production fails the application immediately at startup rather than at the first customer request. The config-vs-code rule is simple: configuration selects which behavior is active (flags, URLs, timeouts), and code defines how that behavior works (algorithms, business rules).
In National Bank of India's forex rate service, Pankaj uses three profiles: dev with fixed mock rates and no external API calls, staging with the UAT API, and prod with the live API and secrets injected from Kubernetes. The rate-provider property selects between LiveRateProvider and MockRateProvider via @ConditionalOnProperty, requiring zero if-else logic in code. Spring Cloud Config centralizes configuration across all microservices so changes propagate from a single Git repository.
Remember one sentence: CLI beats environment variable beats profile file beats base file, and @ConfigurationProperties validates your config at startup so Priya never hits a misconfigured service.
---