Profile-specific Configuration

Override settings per environment with application-{profile} files, multi-document YAML and profile groups, without duplicating everything.

Syntaxapplication.yaml # always loaded application-{profile}.yaml # loaded when {profile} is active, overrides the above --- spring.config.activate.on-profile: prod spring.profiles.group.prod=proddb,prodmail

A profile is a named set of configuration, such as dev, test or prod. The Core Spring lessons showed @Profile switching beans on and off; this lesson is about switching property values. The pattern is the same in every project: shared defaults in application.yaml, and only the differences in a profile-specific file.

Profile-specific files

When the prod profile is active, Boot loads application.yaml and then application-prod.yaml from the same locations. Values in the profile file override the shared ones; everything else is inherited. Activate profiles with spring.profiles.active, usually from the environment: SPRING_PROFILES_ACTIVE=prod. With several active profiles (prod,eu), the last one listed wins when they set the same key.

Multi-document files

One YAML file can hold several documents separated by ---. A document containing spring.config.activate.on-profile: prod applies only when prod is active. Properties files do the same with a #--- line. This keeps small overrides next to the defaults, but a file with five environments in it becomes hard to review; separate files scale better.

Profile groups

spring.profiles.group.prod=proddb,prodmail means activating prod also activates the two others. It lets you split configuration by concern (database, mail, observability) and still start each environment with a single name.

Rules that trip people up

  • You cannot set spring.profiles.active or spring.profiles.include inside a profile-specific file or document. Boot rejects it at startup, because a profile cannot activate itself.
  • Files next to the JAR beat files inside it, profile-specific or not. An old application.properties left on a server therefore overrides application-prod.yaml packaged in the JAR.
  • If no profile is set, the default profile is active, and application-default.yaml is loaded.

How to use them well

Keep the number of profiles small and name them after environments or capabilities, not after people. Do not put production credentials in application-prod.yaml: a profile file is still a file in the repository. Use it for non-secret differences (pool sizes, log levels, feature flags), and pull secrets from the environment, as the last lesson in this category shows.

Make the shared file safe to run locally, and make production the deliberate choice. An application that connects to the production database because nobody set a profile is a real incident, not a theoretical one.

Example

Example · yaml
# application.yaml: defaults that are safe on a laptop
spring:
  application:
    name: orders-api
  datasource:
    url: jdbc:postgresql://localhost:5432/orders
    hikari:
      maximum-pool-size: 5
logging:
  level:
    com.example.orders: DEBUG
app:
  emails:
    enabled: false
---
# Applies only when the prod profile is active
spring:
  config:
    activate:
      on-profile: prod
  datasource:
    url: jdbc:postgresql://orders-db.internal:5432/orders
    hikari:
      maximum-pool-size: 20
logging:
  level:
    com.example.orders: INFO
app:
  emails:
    enabled: true

When to use it

  • Running a laptop against a local Postgres with debug logging while production uses a larger pool and INFO logging, from one shared file.
  • Redirecting every outgoing email in staging to a QA inbox so test orders never reach real customers.
  • Starting production with one profile name that pulls in separate database, mail and observability profile groups.

More examples

A separate file per environment

The staging file sets four keys and inherits everything else, so a change to a shared default reaches every environment at once. Profile groups belong in the shared file: activating `prod` there turns on `proddb` and `prodmail`, whose own files hold the database and mail settings. Never add a production group such as `proddb` to staging: group members are applied after the profile that pulls them in, so its database URL would override the staging one.

Example · properties
# application-staging.properties: only the differences from application.properties
spring.datasource.url=jdbc:postgresql://orders-db.staging.internal:5432/orders
spring.datasource.hikari.maximum-pool-size=10
logging.level.com.example.orders=INFO
app.emails.enabled=true
[email protected]

# application.properties (shared) declares the groups
# spring.profiles.group.prod=proddb,prodmail

Beans and values that differ by profile

Profile-specific properties change values; `@Profile` changes which bean exists. Here a laptop logs emails instead of sending them and staging redirects every message to a QA inbox, so no customer receives a test email. The `!staging & !prod` expression makes the safe option the default. Production would declare its real SMTP-backed sender under `@Profile("prod")`.

Example · java
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

interface EmailSender {
    void send(String to, String subject, String body);
}

class LoggingEmailSender implements EmailSender {
    @Override
    public void send(String to, String subject, String body) {
        System.out.println("Would send '" + subject + "' to " + to);
    }
}

class RedirectingEmailSender implements EmailSender {
    private final EmailSender delegate;
    private final String redirectTo;

    RedirectingEmailSender(EmailSender delegate, String redirectTo) {
        this.delegate = delegate;
        this.redirectTo = redirectTo;
    }

    @Override
    public void send(String to, String subject, String body) {
        delegate.send(redirectTo, "[for " + to + "] " + subject, body);
    }
}

@Configuration(proxyBeanMethods = false)
public class EmailConfig {

    @Bean
    @Profile("!staging & !prod")
    EmailSender devEmailSender() {
        return new LoggingEmailSender();
    }

    @Bean
    @Profile("staging")
    EmailSender stagingEmailSender(@Value("${app.emails.redirect-all-to}") String redirectTo) {
        return new RedirectingEmailSender(new LoggingEmailSender(), redirectTo);
    }
}

Activating profiles

Always read the startup line that lists active profiles after a deployment. A typo such as `SPRING_PROFILES_ACTIVE=prd` does not fail; the app simply starts with the shared settings, which is why the shared file should never point at production.

Example · bash
# Environment variable: the usual choice for containers and servers
SPRING_PROFILES_ACTIVE=prod java -jar orders-api.jar

# Command-line argument: handy locally, wins over the variable
java -jar orders-api.jar --spring.profiles.active=staging

# Maven during development
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev

# The startup log confirms what is active: look for the line
# "The following ... profiles are active: ..." right after the banner

Discussion

  • Be the first to comment on this lesson.