Spring Boot Profiles and Environment-Specific Configuration

Introduction

This tutorial explains profiles in Spring Boot - named sets of configuration, such as dev and prod, that let the same application run with different settings and beans in each environment. We will start with what a profile is and write profile-specific configuration files. We will then activate profiles in the ways Spring Boot offers - a property, a command-line argument and an environment variable - and see how values are merged, including what happens when two active profiles set the same key. Next we will use @Profile to switch beans, group profiles together, and put several profiles into one YAML file. Lastly, we will see a common mistake that Spring Boot refuses at startup, activate profiles in tests, and list best practices.

(The configuration files and their order of precedence are explained in application.properties vs application.ymlClick to read tutorial: application.properties vs application.yml in Spring Boot; @Profile on beans was introduced in @Qualifier, @Primary and conditional beansClick to read tutorial: Spring @Qualifier, @Primary and Conditional Beans.)

What is a profile?

A real application runs in several environments - a developer's laptop, a test server, production - and some settings must differ between them: the database URL, whether to send real emails, whether caching is on. A profile is a name for one of those environments. You write the settings that differ into files named after the profile, and when the application starts with that profile active, Spring Boot loads those files on top of the common ones. The same jar then behaves correctly everywhere, without a rebuild.

Profile-specific configuration files

A file named application-{profile}.properties (or .yml) is loaded only when {profile} is active. The common file holds the defaults for every environment -
application.properties - settings for every environment
shop.name=JavaBrahman Books
shop.database-url=jdbc:h2:mem:shop
shop.cache-enabled=false
shop.region=GLOBAL
shop.payments=sandbox
application-dev.properties - only when the dev profile is active
shop.database-url=jdbc:h2:mem:devshop
shop.banner=*** DEVELOPMENT - test data only ***
application-prod.properties - only when the prod profile is active
shop.database-url=jdbc:postgresql://db.internal:5432/shop
shop.cache-enabled=true
shop.payments=live
shop.region=US
application-eu.properties - settings for the European deployment
shop.region=EU
shop.currency=EUR
The application prints its active profiles and the resulting settings, and uses a profile-dependent bean -
An application that prints its profiles, settings and profile-specific bean
//ProfilesApp.java
package com.javabrahman.boot.profiles;

import java.util.Arrays;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Profile;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

interface MailSender {
  String describe();
}

@Component
@Profile("prod")
class SmtpMailSender implements MailSender {
  public String describe() {
    return "SmtpMailSender (real emails)";
  }
}

@Component
@Profile("!prod")
class LoggingMailSender implements MailSender {
  public String describe() {
    return "LoggingMailSender (emails written to the log only)";
  }
}

@SpringBootApplication
public class ProfilesApp {
  public static void main(String[] args) {
    try (var context = SpringApplication.run(ProfilesApp.class, args)) {
      Environment env = context.getEnvironment();
      System.out.println("active profiles    = " + Arrays.toString(env.getActiveProfiles())
          + (env.getActiveProfiles().length == 0 ? " (default profiles " + Arrays.toString(env.getDefaultProfiles()) + ")" : ""));
      for (String key : new String[] {"shop.database-url", "shop.cache-enabled", "shop.payments", "shop.region", "shop.currency", "shop.banner"}) {
        String value = env.getProperty(key);
        if (value != null) {
          System.out.printf("%-19s= %s%n", key, value);
        }
      }
      System.out.println("mail sender        = " + context.getBean(MailSender.class).describe());
    }
  }
}

Activating profiles

The standard property is spring.profiles.active, a comma-separated list of profile names. Like any property, it can be set in a file, as a JVM system property, as an environment variable or on the command line. First with no profile, then with dev, then with prod -  OUTPUT with no profile active
active profiles    = [] (default profiles [default])
shop.database-url  = jdbc:h2:mem:shop
shop.cache-enabled = false
shop.payments      = sandbox
shop.region        = GLOBAL
mail sender        = LoggingMailSender (emails written to the log only)
 OUTPUT with --spring.profiles.active=dev
active profiles    = [dev]
shop.database-url  = jdbc:h2:mem:devshop
shop.cache-enabled = false
shop.payments      = sandbox
shop.region        = GLOBAL
shop.banner        = *** DEVELOPMENT - test data only ***
mail sender        = LoggingMailSender (emails written to the log only)
 OUTPUT with --spring.profiles.active=prod
active profiles    = [prod]
shop.database-url  = jdbc:postgresql://db.internal:5432/shop
shop.cache-enabled = true
shop.payments      = live
shop.region        = US
mail sender        = SmtpMailSender (real emails)

Explanation of the output

  • With no active profile, Spring Boot uses the default profile, named default, and only application.properties applied. (A file named application-default.properties would be loaded in this case; spring.profiles.default changes which profiles count as default.)
  • With dev, application-dev.properties was loaded on top of the common file: it replaced the database URL and added a banner, and every other setting kept its common value. Profile files only need to contain what differs.
  • With prod, the production values replaced four settings, and @Profile("prod") switched the mail sender to the SMTP implementation. @Profile("!prod") made the logging sender the choice everywhere else, so exactly one MailSender bean existed in each run.

Several active profiles: the last one wins

More than one profile can be active at once - for example the environment (prod) and the region (eu). Both application-prod.properties and application-eu.properties set shop.region, so the order matters -  OUTPUT with --spring.profiles.active=prod,eu
active profiles    = [prod, eu]
shop.database-url  = jdbc:postgresql://db.internal:5432/shop
shop.cache-enabled = true
shop.payments      = live
shop.region        = EU
shop.currency      = EUR
mail sender        = SmtpMailSender (real emails)
 OUTPUT with --spring.profiles.active=eu,prod
active profiles    = [eu, prod]
shop.database-url  = jdbc:postgresql://db.internal:5432/shop
shop.cache-enabled = true
shop.payments      = live
shop.region        = US
shop.currency      = EUR
mail sender        = SmtpMailSender (real emails)
Both runs merged the settings of both profiles - production database, live payments, and the euro from the eu file. For the one key both files set, shop.region, the profile listed last won: EU in the first run, US in the second. Design profiles so that they rarely set the same key, and when they do, document the intended order.

Activating a profile with an environment variable

On servers and in containers, profiles are usually activated with the SPRING_PROFILES_ACTIVE environment variable - relaxed binding maps it to spring.profiles.active - so that the start command stays the same everywhere -
set SPRING_PROFILES_ACTIVE=dev
java -jar shop.jar
 OUTPUT with SPRING_PROFILES_ACTIVE=dev
active profiles    = [dev]
shop.database-url  = jdbc:h2:mem:devshop
shop.cache-enabled = false
shop.payments      = sandbox
shop.region        = GLOBAL
shop.banner        = *** DEVELOPMENT - test data only ***
mail sender        = LoggingMailSender (emails written to the log only)
The result is the same as with the command-line argument. In a Dockerfile or a Kubernetes deployment, the variable goes in the container's environment; in a Maven run, ./mvnw spring-boot:run -Dspring-boot.run.profiles=dev does the same.

Profile groups

As the number of profiles grows, activating the right combination gets error-prone. A profile group, defined in the common configuration file, gives one name to several profiles. Running the same application with an extra group definition on the command line -  OUTPUT with the group production=prod,eu activated
active profiles    = [production, prod, eu]
shop.database-url  = jdbc:postgresql://db.internal:5432/shop
shop.cache-enabled = true
shop.payments      = live
shop.region        = EU
shop.currency      = EUR
mail sender        = SmtpMailSender (real emails)
Activating production also activated its members, prod and eu, in that order. In a real project the group is a line in application.properties, such as spring.profiles.group.production=prod,eu,metrics; it is passed on the command line here only to keep the example's files unchanged.

Several profiles in one YAML file

Instead of one file per profile, a single YAML file can hold several documents, separated by ---. A document that starts with spring.config.activate.on-profile applies only when that profile is active (.properties files support the same with a #--- separator line) -
multi.yml - common settings and two profile documents in one file
shop:
  database-url: jdbc:h2:mem:shop
  payments: sandbox
  region: GLOBAL
---
spring:
  config:
    activate:
      on-profile: dev
shop:
  banner: "*** DEVELOPMENT - from multi.yml ***"
---
spring:
  config:
    activate:
      on-profile: prod
shop:
  database-url: jdbc:postgresql://db.internal:5432/shop
  payments: live
 OUTPUT with --spring.config.name=multi --spring.profiles.active=prod
active profiles    = [prod]
shop.database-url  = jdbc:postgresql://db.internal:5432/shop
shop.payments      = live
shop.region        = GLOBAL
mail sender        = SmtpMailSender (real emails)
Only the first document and the prod document applied; the dev document was skipped, so there is no banner. One file per profile is easier to find and to deploy separately; one multi-document file keeps small differences side by side. Both are fine - choose one style per project.

A mistake Spring Boot refuses

It is tempting to activate one profile from inside another profile's file. Spring Boot does not allow spring.profiles.active in a profile-specific file, because the set of active profiles has to be known before profile-specific files can be chosen - and it says so at startup -
application-qa.properties - wrongly tries to activate another profile
shop.database-url=jdbc:h2:mem:qashop
spring.profiles.active=dev
Starting the application with the qa profile
//QaStartup.java
package com.javabrahman.boot.profiles;

import org.springframework.boot.SpringApplication;

public class QaStartup {
  public static void main(String[] args) {
    try (var context = SpringApplication.run(ProfilesApp.class, "--spring.profiles.active=qa")) {
      System.out.println("Started");
    } catch (Exception e) {
      System.out.println(e.getClass().getSimpleName() + ":\n  " + e.getMessage());
    }
  }
}
 OUTPUT with --spring.profiles.active=qa (the lines the program prints; Spring Boot also logs the full stack trace)
InvalidConfigDataPropertyException:
  Property 'spring.profiles.active' imported from location 'class path resource [application-qa.properties]' is invalid in a profile specific resource [origin: class path resource [application-qa.properties] - 2:24]
Startup stopped before any bean was created. The message names the property, the file it came from, and its line and column (2:24). Because the failure happens before logging is configured, Spring Boot also prints the whole stack trace, which is left out above. The fix is a profile group: spring.profiles.group.qa=dev in the common file activates dev whenever qa is active.

Profiles in tests

Tests often need their own settings - an in-memory database, fake external services. @ActiveProfiles activates profiles for one test class, which then loads an application-test.properties from src/test/resources along with @Profile("test") beans -
Activating a profile in a Spring Boot test
@SpringBootTest
@ActiveProfiles("test")
class CheckoutServiceTests {
  // runs with application.properties + application-test.properties
}

Best practices

  • Keep the common file complete and the profile files small: a profile file should contain only what differs.
  • Prefer configuration over code: switch settings with profiles, and use @Profile beans only when the behaviour itself differs, as with the mail sender. Avoid if (environment.acceptsProfiles(...)) branches in business code.
  • Do not put secrets in profile files that are committed to source control. Supply passwords and keys through environment variables or a secrets manager, and let the profile file refer to them with placeholders such as ${DB_PASSWORD}.
  • Name profiles after environments or capabilities (dev, prod, eu, metrics), and use groups for combinations.
  • Make the default safe: with no profile active, the application should never charge real cards or send real emails - as shop.payments=sandbox and the logging mail sender ensure here.

Summary

In this tutorial we first saw what a profile is and wrote profile-specific configuration files on top of a common application.properties. We activated profiles with a command-line argument and an environment variable, saw the default profile, and watched @Profile choose between two mail senders. We then activated two profiles at once and saw that the last one wins for a shared key, grouped profiles under one name, and put several profiles into one multi-document YAML file. Lastly, we saw Spring Boot refuse spring.profiles.active inside a profile-specific file, activated profiles in tests, and listed best practices. The next tutorial covers what an application tells you while it runs: logging in Spring BootClick to read tutorial: Logging in Spring Boot: SLF4J, Logback and Log Levels.