Application Configuration: Properties, YAML, and Profiles

Every real application needs different configuration for different environments — a local database for dev, a connection pool for staging, a secret manager for prod. This article covers everything Spring Boot gives you to handle this cleanly.

application.properties vs application.yml

Spring Boot reads configuration from src/main/resources/application.properties (or .yml) by default. Both formats express the same thing:

application.properties:

server.port=8080
spring.datasource.url=jdbc:postgresql://localhost:5432/orders
spring.datasource.username=app
spring.datasource.password=secret
spring.jpa.show-sql=false

application.yml:

server:
  port: 8080
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/orders
    username: app
    password: secret
  jpa:
    show-sql: false

YAML is generally preferred for nested properties — it’s less repetitive. Both are functionally equivalent. Pick one and be consistent.

The Property Hierarchy

Spring Boot resolves properties from many sources, in a specific order. Later sources override earlier ones. The most important levels are:

Priority (lowest → highest)
────────────────────────────────────
1. Default values in @ConfigurationProperties
2. application.properties / application.yml
3. Profile-specific: application-{profile}.properties
4. Environment variables
5. System properties (-Dproperty=value)
6. Command-line arguments (--property=value)
────────────────────────────────────

This means you can always override config from outside the JAR — critical for containers and CI/CD.

# Override at runtime without rebuilding
java -jar order-service.jar \
  --spring.datasource.url=jdbc:postgresql://prod-db:5432/orders \
  --server.port=9090

Profiles: Environment-Specific Configuration

Profiles let you have different configuration per environment without changing your code.

Creating Profile-Specific Files

Create files named application-{profile}.properties or application-{profile}.yml:

src/main/resources/
  application.yml           ← base config (all environments)
  application-dev.yml       ← dev overrides
  application-staging.yml   ← staging overrides
  application-prod.yml      ← prod overrides

application.yml (base):

server:
  port: 8080

app:
  order:
    max-items: 50
    currency: USD

spring:
  jpa:
    hibernate:
      ddl-auto: validate

application-dev.yml:

spring:
  datasource:
    url: jdbc:h2:mem:orders
    driver-class-name: org.h2.Driver
    username: sa
    password: ""
  h2:
    console:
      enabled: true
  jpa:
    hibernate:
      ddl-auto: create-drop   # recreate schema on restart in dev
    show-sql: true

logging:
  level:
    com.devopsmonk: DEBUG
    org.springframework.web: DEBUG

application-prod.yml:

spring:
  datasource:
    url: ${DB_URL}             # from environment variable
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5

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

Activating a Profile

Via command-line (recommended for local dev):

java -jar order-service.jar --spring.profiles.active=dev

Via environment variable (recommended for containers):

export SPRING_PROFILES_ACTIVE=prod
java -jar order-service.jar

Via application.properties (for default local profile):

# Only set this in your local application.properties, never commit it
spring.profiles.active=dev

Via Maven at build time:

./mvnw spring-boot:run -Dspring-boot.run.profiles=dev

Profile Groups (Spring Boot 2.4+)

Group multiple profiles together:

spring:
  profiles:
    group:
      prod: prod-db,prod-logging,prod-monitoring
      staging: staging-db,staging-logging

Now --spring.profiles.active=prod activates all three prod profiles at once.

Environment Variable Binding

Spring Boot automatically maps environment variables to properties using relaxed binding:

Environment VariableProperty
SPRING_DATASOURCE_URLspring.datasource.url
SERVER_PORTserver.port
APP_ORDER_MAX_ITEMSapp.order.max-items

This is how you configure apps in Kubernetes without modifying the JAR:

# kubernetes deployment.yaml
env:
  - name: SPRING_PROFILES_ACTIVE
    value: prod
  - name: DB_URL
    valueFrom:
      secretKeyRef:
        name: db-secret
        key: url
  - name: DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: db-secret
        key: password

@ConfigurationProperties — The Right Way to Read Config

Avoid injecting individual properties with @Value. Instead, bind a whole group of related properties to a typed class.

@ConfigurationProperties(prefix = "app.order")
@Validated
public class OrderProperties {

    @NotNull
    private String currency = "USD";

    @Min(1)
    @Max(1000)
    private int maxItemsPerOrder = 50;

    @DurationUnit(ChronoUnit.SECONDS)
    private Duration processingTimeout = Duration.ofSeconds(30);

    private RetryConfig retry = new RetryConfig();

    // Nested config
    public static class RetryConfig {
        private int maxAttempts = 3;
        private Duration backoff = Duration.ofMillis(500);
        // getters and setters
    }

    // getters and setters
}

Register it:

@SpringBootApplication
@EnableConfigurationProperties(OrderProperties.class)
public class OrderServiceApplication { ... }

Or just add @Component to the properties class itself.

Configure it in YAML:

app:
  order:
    currency: EUR
    max-items-per-order: 100
    processing-timeout: 60s
    retry:
      max-attempts: 5
      backoff: 1000ms

Inject it:

@Service
public class OrderService {

    private final OrderProperties props;

    public OrderService(OrderProperties props) {
        this.props = props;
    }

    public Order createOrder(OrderRequest request) {
        if (request.items().size() > props.getMaxItemsPerOrder()) {
            throw new OrderTooLargeException(
                "Max " + props.getMaxItemsPerOrder() + " items per order"
            );
        }
        // ...
    }
}

Why @ConfigurationProperties over @Value:

  • Type-safe (Duration, List, Map — not just String)
  • Validated at startup — fail fast on bad config
  • IDE autocomplete (with spring-boot-configuration-processor)
  • Testable (just instantiate and set values)
  • Relaxed binding (camelCase, kebab-case, UPPER_SNAKE_CASE all work)

IDE Autocomplete for Custom Properties

Add the annotation processor:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

After rebuilding, your IDE shows autocomplete and documentation for app.order.* properties in application.yml.

Profile-Specific Beans

Sometimes you don’t just need different config — you need different implementations. Use @Profile:

// Used in dev — logs to console, never hits SES
@Component
@Profile("dev")
public class ConsoleEmailService implements EmailService {
    public void send(Email email) {
        log.info("DEV: Would send email to {} — subject: {}", email.to(), email.subject());
    }
}

// Used in prod — real SES integration
@Component
@Profile("prod")
public class SesEmailService implements EmailService {
    private final SesClient ses;

    public void send(Email email) {
        ses.sendEmail(/* ... */);
    }
}

Only one is active at a time. The OrderService injects EmailService — it doesn’t care which one.

You can also use @Profile with logical expressions:

@Component
@Profile("!prod")   // active in any profile that isn't prod
public class DevToolsConfig { ... }

@Component
@Profile("prod | staging")   // active in prod OR staging
public class CloudMonitoring { ... }

Secrets: What Not to Put in Config Files

Never commit real secrets to application.properties or application.yml. These files go into git.

For local dev: use application-local.yml (add it to .gitignore):

# application-local.yml — NEVER commit this
spring:
  datasource:
    password: mysecretlocalpassword

For CI/CD and production: inject secrets as environment variables:

spring:
  datasource:
    password: ${DB_PASSWORD}  # reads from env var

For production at scale: use a secrets manager:

  • AWS Secrets Manager → spring-cloud-aws
  • HashiCorp Vault → spring-cloud-vault
  • Kubernetes Secrets → mounted as env vars or files

Practical: Configuring the order-service for Three Environments

Here’s what the complete config setup looks like for the sample order-service:

src/main/resources/
  application.yml          ← common defaults
  application-dev.yml      ← H2 in-memory, SQL logging, debug level
  application-prod.yml     ← PostgreSQL from env vars, connection pool tuned

application.yml:

server:
  port: 8080
  shutdown: graceful

spring:
  application:
    name: order-service
  jpa:
    open-in-view: false
    hibernate:
      ddl-auto: validate

app:
  order:
    currency: USD
    max-items-per-order: 50
    processing-timeout: 30s

application-dev.yml:

spring:
  datasource:
    url: jdbc:h2:mem:orderdb;DB_CLOSE_DELAY=-1
    driver-class-name: org.h2.Driver
    username: sa
    password:
  h2.console.enabled: true
  jpa:
    show-sql: true
    hibernate.ddl-auto: create-drop

logging:
  level:
    com.devopsmonk: DEBUG

application-prod.yml:

spring:
  datasource:
    url: ${DB_URL}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus

Run locally with dev profile:

./mvnw spring-boot:run -Dspring-boot.run.profiles=dev

Run in prod (Docker):

docker run \
  -e SPRING_PROFILES_ACTIVE=prod \
  -e DB_URL=jdbc:postgresql://db:5432/orders \
  -e DB_USERNAME=app \
  -e DB_PASSWORD=secret \
  order-service:latest

Checking Active Configuration

During development, use the Actuator env endpoint to see resolved values:

management.endpoints.web.exposure.include=env,configprops
# See all resolved properties and their sources
curl http://localhost:8080/actuator/env | jq .

# See a specific property
curl http://localhost:8080/actuator/env/spring.datasource.url | jq .

The configprops endpoint shows all @ConfigurationProperties beans and their current values.

What You’ve Learned

  • application.yml and application.properties are equivalent — YAML is preferred for nested config
  • Profile-specific files (application-dev.yml, application-prod.yml) override base config per environment
  • Environment variables override file-based config — essential for containers
  • @ConfigurationProperties binds groups of related properties to typed, validated classes
  • @Profile switches beans at runtime — use it for environment-specific implementations
  • Never commit secrets; inject them as environment variables or use a secrets manager

Next: Article 6 — Spring IoC and Dependency Injection — the container that makes all of this work.