Profile-specific Configuration
Override settings per environment with application-{profile} files, multi-document YAML and profile groups, without duplicating everything.
application.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,prodmailA 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.activeorspring.profiles.includeinside 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.propertiesleft on a server therefore overridesapplication-prod.yamlpackaged in the JAR. - If no profile is set, the
defaultprofile is active, andapplication-default.yamlis 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
# application.yaml: defaults that are safe on a laptop
spring
application
nameorders-api
datasource
urljdbcpostgresql//localhost5432/orders
hikari
maximum-pool-size5
logging
level
com.example.ordersDEBUG
app
emails
enabledfalse
---
# Applies only when the prod profile is active
spring
config
activate
on-profileprod
datasource
urljdbcpostgresql//orders-db.internal5432/orders
hikari
maximum-pool-size20
logging
level
com.example.ordersINFO
app
emails
enabledtrueWhen 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.
# 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,prodmailBeans 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")`.
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.
# 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