Back to Configuration & Profiles
Q2Question 2 of 2

How does property resolution work in Spring Boot?

Key Concepts
  • Properties resolve in strict priority: CLI args > JVM props > env vars > profile-specific files > base application.yml > defaults
  • Profiles activate environment-specific configuration: application-prod.yml overlays application.yml
  • @ConfigurationProperties binds grouped properties to a type-safe immutable record with startup-time validation
  • @Value is fine for simple one-off values but lacks type safety and validation
  • Config selects behavior (flags, URLs, timeouts) while code defines behavior (algorithms, business rules)
  • Spring Cloud Config centralizes configuration across microservices with a Git-backed server
  • Relaxed binding maps kebab-case properties to camelCase fields automatically

Definition

Spring Boot resolves properties in a strict priority order where command-line arguments win over environment variables, which win over profile-specific files, which win over the base application.yml, and @ConfigurationProperties binds these into type-safe validated objects.
  • Property resolution order – CLI args (highest), JVM system properties, OS environment variables, profile-specific application-{profile}.yml, base application.yml, @PropertySource, @Value defaults (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 @Validated for 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.

  1. Command-line arguments (--server.port=9090) : highest priority
  2. JVM system properties (-Dserver.port=9090)
  3. OS environment variables (SERVER_PORT=9090 : note the UPPER_SNAKE_CASE mapping)
  4. Profile-specific application-{profile}.yml files
  5. Base application.yml or application.properties
  6. @PropertySource annotations on configuration classes
  7. 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.

JAVA
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);
    }
}
BASH
# 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
TEXT
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.

YAML
# 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
BASH
# 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
JAVA
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.

JAVA
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
    ) {}
}
YAML
# 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
JAVA
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);
    }
}
TEXT
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 |

JAVA
// @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 |

YAML
# 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
JAVA
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.

JAVA
// 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.

DIAGRAM
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│
  └───────────────┘ └───────────────┘ └───────────────┘
YAML
# 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.import to import additional configuration sources, including Spring Cloud Config Server and Vault for secrets management.
  • @ConfigurationProperties supports constructor binding by default with Java records, making configuration objects immutable and thread-safe.
  • Use spring-boot-configuration-processor to generate metadata so IDEs provide autocomplete for your custom properties.
  • Secrets should never be stored in application.yml files or Git repositories. Use Kubernetes secrets, HashiCorp Vault, or AWS Secrets Manager, and reference them via environment variables.
  • Pankaj uses @ConfigurationPropertiesScan on the main class to automatically discover all @ConfigurationProperties records without needing @EnableConfigurationProperties.
JAVA
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,metrics activates 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, or matchIfMissing = true on @ConditionalOnProperty.
  • What is the difference between application.yml and application.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

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate