Spring Boot Configuration Management — Profiles, @ConfigurationProperties, and Secrets

by Arif Ikhsanudin, Backend Developer

Property source precedence — the complete ordering

Spring Boot loads properties from multiple sources, evaluated in a fixed precedence order. Higher-priority sources override lower-priority ones. The full ordering (highest to lowest):

  1. Command-line arguments (--server.port=9090)
  2. SPRING_APPLICATION_JSON environment variable (JSON blob of properties)
  3. Servlet init parameters
  4. JNDI attributes
  5. Java system properties (-Dserver.port=9090)
  6. OS environment variables (SERVER_PORT=9090)
  7. Profile-specific properties outside the JAR (application-{profile}.properties)
  8. Application properties outside the JAR (application.properties)
  9. Profile-specific properties inside the JAR
  10. Application properties inside the JAR (application.yml)
  11. @PropertySource annotations on @Configuration classes
  12. Default properties (SpringApplication.setDefaultProperties)

The practical implications:

Environment variables override everything in the JAR. DATABASE_URL set in the container environment overrides spring.datasource.url in application.yml. This is the intended behavior for containerized deployments — the JAR carries defaults, the environment carries production values.

@PropertySource is low-priority. Properties loaded via @PropertySource cannot override application.properties or environment variables. Teams that use @PropertySource for environment-specific configuration are surprised when environment variables don't override as expected — they do, @PropertySource just can't override application.properties, not the other way around.

Profile-specific files override the base file. application-production.yml overrides application.yml when the production profile is active. This is the correct mechanism for environment-specific overrides.

@ConfigurationProperties — typed, validated configuration

@Value("${some.property}") is the simple approach. It doesn't validate, doesn't provide IDE completion in many IDEs without additional setup, and scatters property usage throughout the codebase. For any non-trivial configuration, @ConfigurationProperties is better:

@ConfigurationProperties(prefix = "app.payment")
@Validated
public class PaymentConfig {

    @NotBlank
    private String apiKey;

    @NotNull
    @PositiveOrZero
    private Integer maxRetries = 3;

    @NotNull
    @DurationMin(seconds = 1)
    private Duration timeout = Duration.ofSeconds(30);

    @NotBlank
    private String webhookSecret;

    @Valid
    private CircuitBreaker circuitBreaker = new CircuitBreaker();

    // Getters and setters (or use records with Spring Boot 3.x)

    public static class CircuitBreaker {
        @Positive
        private int failureThreshold = 5;

        @NotNull
        private Duration waitDuration = Duration.ofSeconds(60);

        // Getters and setters
    }
}
app:
  payment:
    api-key: ${PAYMENT_API_KEY}
    max-retries: 5
    timeout: 45s
    webhook-secret: ${PAYMENT_WEBHOOK_SECRET}
    circuit-breaker:
      failure-threshold: 10
      wait-duration: 30s

Register the configuration class:

@Configuration
@EnableConfigurationProperties(PaymentConfig.class)
public class PaymentModuleConfig {
    // or use @ConfigurationPropertiesScan on the main class
}

Or add @Component to the @ConfigurationProperties class (simpler but couples the configuration to Spring component scanning).

Validation. @Validated on the class triggers Bean Validation on the properties when the context starts. A missing apiKey or an invalid timeout causes the application to fail at startup with a clear message — not a NullPointerException when the code first tries to use the value.

Type conversion. Spring Boot converts property strings to typed values automatically: "45s" to Duration.ofSeconds(45), "10MB" to DataSize.ofMegabytes(10), "true" to boolean. Use these types in @ConfigurationProperties classes rather than storing strings and parsing manually.

Relaxed binding. app.payment.api-key, app.payment.apiKey, APP_PAYMENT_API_KEY, and APP_PAYMENT_APIKEY all map to the apiKey field. Spring Boot's relaxed binding handles camelCase, kebab-case, and SCREAMING_SNAKE_CASE — use kebab-case in property files (api-key) and SCREAMING_SNAKE_CASE for environment variables (API_KEY).

Profiles — environment-specific configuration

Spring profiles enable loading different configuration for different environments. The recommended structure:

src/main/resources/
  application.yml            # shared across all environments
  application-dev.yml        # development overrides
  application-staging.yml    # staging overrides
  application-prod.yml       # production overrides
# application.yml — base configuration, environment-neutral
spring:
  application:
    name: order-service
  jpa:
    open-in-view: false
    hibernate:
      ddl-auto: validate

server:
  port: 8080
  shutdown: graceful

management:
  server:
    port: 8081
# application-dev.yml — development overrides
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/orderdb_dev
    username: devuser
    password: devpassword
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: update  # more permissive in development

logging:
  level:
    com.example: DEBUG
    org.hibernate.SQL: DEBUG
# application-prod.yml — production overrides (no secrets — those come from environment)
spring:
  datasource:
    url: ${DATABASE_URL}
    username: ${DATABASE_USERNAME}
    password: ${DATABASE_PASSWORD}
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
  jpa:
    show-sql: false

logging:
  level:
    root: WARN
    com.example: INFO

The production profile never contains secrets. Property values for DATABASE_URL, API keys, and credentials reference environment variables (${VARIABLE_NAME}). The environment (Kubernetes Secret, AWS SSM, Vault) provides the actual values.

Activate profiles:

# Environment variable (recommended for containers)
SPRING_PROFILES_ACTIVE=prod

# Command-line argument
java -jar app.jar --spring.profiles.active=prod

# In application.yml (for always-on profiles)
spring:
  profiles:
    active: prod

Multiple profiles. SPRING_PROFILES_ACTIVE=prod,feature-flags-v2 activates both profiles. Profile-specific files are processed in order — application-feature-flags-v2.yml properties override application-prod.yml properties for overlapping keys.

@Value vs @ConfigurationProperties — when to use each

@Value is appropriate for a single property used in one place:

@Service
public class EmailService {

    @Value("${app.email.from-address}")
    private String fromAddress;

    // ...
}

@ConfigurationProperties is appropriate when:

  • Multiple related properties belong together
  • Properties are used in more than one class (bind once, inject the config class)
  • Validation of the configuration is required
  • IDE support and documentation generation matter

The signal to refactor from @Value to @ConfigurationProperties: a class with more than two @Value fields, or the same property referenced in multiple classes.

Secrets — what not to do and what to do instead

Never commit secrets to version control. application-prod.yml with actual credentials is a security incident. Even if the repository is private, secrets in version control create risk: repository access grants, CI logs, git history after credential rotation.

Never log configuration properties at startup. spring.boot.admin.logging.enabled=true and similar logging of environment details may expose secrets in log aggregators. Spring Boot's Sanitizer redacts common secret patterns (keys containing password, secret, token, key) from actuator /env output. Add custom sanitizing patterns for application-specific secret names:

@Bean
public SanitizingFunction sanitizingFunction() {
    return data -> {
        String key = data.getKey().toLowerCase();
        if (key.contains("apikey") || key.contains("webhook") || key.contains("credential")) {
            return data.withValue("**REDACTED**");
        }
        return data;
    };
}

The options for secret management:

Environment variables (from Kubernetes Secrets, Docker secrets, ECS task definitions): the simplest approach. The secret is injected into the container environment; Spring Boot reads it as a property. No additional library required. The downside: environment variables are visible to all processes in the container and show up in /proc in some configurations.

Spring Cloud Config Server: a dedicated configuration server that serves properties to applications. Secrets are stored in the server (backed by Git, Vault, or a database); the application fetches them at startup. Adds infrastructure complexity; appropriate for large organizations with many services.

HashiCorp Vault with Spring Cloud Vault: fetches secrets from Vault at startup. Secrets are never stored on disk or in environment variables. Supports dynamic credentials (database credentials that expire and rotate automatically):

spring:
  cloud:
    vault:
      uri: https://vault.internal:8200
      authentication: kubernetes
      kubernetes:
        role: order-service
      generic:
        default-context: order-service

Properties from Vault are available as Spring Boot properties with the same relaxed binding.

AWS Parameter Store / Secrets Manager via spring-cloud-aws:

spring:
  config:
    import: aws-parameterstore:
  aws:
    parameterstore:
      prefix: /order-service/prod

All parameters under /order-service/prod/ are loaded as Spring Boot properties. /order-service/prod/database/password maps to database.password.

Configuration validation at startup

Every @ConfigurationProperties class with @Validated validates at context startup. For properties that fail validation, the application refuses to start with a clear message:

APPLICATION FAILED TO START

Description:
Binding to target org.springframework.boot.context.properties.bind.BindException:
  Failed to bind properties under 'app.payment' to com.example.PaymentConfig
  
Reason: app.payment.api-key must not be blank

This fast-fail at startup — rather than at runtime when the property is first used — is the primary advantage of @ConfigurationProperties with @Validated over @Value. A misconfigured production deployment fails immediately and visibly rather than failing silently on the first request that needs the misconfigured value.

For custom validation logic:

@ConfigurationProperties(prefix = "app.database")
@Validated
public class DatabaseConfig implements InitializingBean {

    private String readReplicaUrl;
    private String primaryUrl;

    @Override
    public void afterPropertiesSet() {
        if (readReplicaUrl != null && readReplicaUrl.equals(primaryUrl)) {
            throw new BeanCreationException(
                "read-replica-url must differ from primary-url");
        }
    }
}

InitializingBean.afterPropertiesSet() runs after the properties are bound — use it for cross-field validation that Bean Validation annotations can't express.

The configuration structure that works at scale

application.yml              # base config, environment-neutral, no secrets
application-dev.yml          # dev database, debug logging, relaxed constraints
application-test.yml         # test database (H2 or Testcontainers config)
application-staging.yml      # staging-specific timeouts, staging service URLs
application-prod.yml         # prod pool sizes, prod logging — NO SECRETS

Secrets arrive via environment variables or a secret manager, never in property files. Profile-specific files contain behavior configuration (log levels, pool sizes, feature flags) but never credentials. The JAR is identical across environments; only the environment configuration changes.

This structure means any developer can read the production profile without gaining access to any credentials. Security teams can audit the property files without worrying about credential exposure. Credential rotation doesn't require a code change or redeploy — update the Kubernetes Secret, restart the pods.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

When Your Environment Is So Locked You Can’t Even Begin

Ever tried to start a project only to hit wall after wall of access restrictions? Sometimes, the environment itself becomes the biggest blocker.

Read more

Why Hiring a Backend Developer in Paris Costs More Than the Salary Suggests

You budgeted €65K for a backend hire. Then your accountant explained cotisations patronales and suddenly the number looked very different.

Read more

How to Transition from Employee to Independent Contractor

Quitting your job sounds exciting… until you realize you have to replace your salary. The shift isn’t just about freedom — it’s about learning how to operate like a business.

Read more

Why “Enjoying WFH” Doesn’t Mean You Deserve Less Pay

Many people assume that working from home is a perk that justifies lower pay. But enjoying the flexibility of WFH doesn’t reduce your skills, effort, or value.

Read more