Configuration, Profiles & Actuator

Profiles in Spring Boot

18 min Lesson 3 of 13

Profiles in Spring Boot

Real applications behave differently in different environments: the database URL in development points to a local container, in staging it points to a shared QA server, and in production it points to a hardened RDS cluster. You do not want to change code between deployments — you want the same JAR to adapt its behaviour based on where it runs. That is exactly what Spring Profiles deliver.

What a Profile Is

A profile is a named group of configuration that Spring activates on demand. When a profile is active, any beans, property files, or configuration classes associated with that profile are loaded. Everything else is ignored. Think of it as a set of overrides applied on top of the base configuration.

Common profile names you will encounter in real projects:

  • dev — local development (H2 in-memory database, verbose logging, no real email)
  • test — automated tests (same H2 or a Testcontainers instance, fast startup)
  • staging — integration environment that mirrors production structure
  • prod — production (real databases, connection pools, secrets from a vault)
Multiple profiles can be active at once. You might activate both prod and eu-west to combine production-grade settings with a region-specific data-residency configuration. Spring merges their properties and beans; later-listed profiles win on conflicts.

Profile-Specific Property Files

The simplest way to supply profile-specific values is with a dedicated properties file. Spring Boot follows an automatic naming convention:

application.properties # base — always loaded application-dev.properties # loaded only when "dev" is active application-staging.properties # loaded only when "staging" is active application-prod.properties # loaded only when "prod" is active

Place all four files in src/main/resources. The base file holds defaults; profile-specific files override or add to them. Here is a typical split:

# application.properties (base) spring.application.name=inventory-service server.port=8080 logging.level.root=INFO
# application-dev.properties spring.datasource.url=jdbc:h2:mem:devdb spring.datasource.username=sa spring.datasource.password= spring.h2.console.enabled=true logging.level.com.example=DEBUG
# application-prod.properties spring.datasource.url=${DB_URL} spring.datasource.username=${DB_USER} spring.datasource.password=${DB_PASS} spring.datasource.hikari.maximum-pool-size=20 logging.level.root=WARN

Notice that application-prod.properties delegates credentials to environment variables via the ${VAR} placeholder syntax. The profile file controls which settings exist; secrets are injected at deploy time.

Activating a Profile

There are several ways to tell Spring Boot which profile to use:

1. Environment variable (preferred in containers and CI/CD):

SPRING_PROFILES_ACTIVE=prod java -jar inventory-service.jar

2. JVM system property:

java -Dspring.profiles.active=prod -jar inventory-service.jar

3. In application.properties (for local defaults only):

spring.profiles.active=dev

4. In tests with @ActiveProfiles:

import org.springframework.test.context.ActiveProfiles; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest @ActiveProfiles("test") class OrderServiceTest { // Spring loads application-test.properties automatically }
Never commit spring.profiles.active=prod to a shared properties file. The active profile is an operational decision, not a code decision. Set it in your deployment pipeline (Docker ENV, Kubernetes env var, CI/CD step) so the same artifact can be promoted from staging to production without rebuilding.

Profile-Specific Beans with @Profile

Profiles are not only for properties. You can gate entire Spring beans on a profile using the @Profile annotation. This is powerful when you need a completely different implementation in different environments — for example, a real email sender in production and a no-op stub locally.

import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; public interface NotificationService { void send(String recipient, String message); } // Only registered when the "prod" profile is active @Service @Profile("prod") public class SmtpNotificationService implements NotificationService { @Override public void send(String recipient, String message) { // real SMTP delivery System.out.println("Sending email to " + recipient); } } // Active in every profile EXCEPT prod (note the ! negation) @Service @Profile("!prod") public class LoggingNotificationService implements NotificationService { @Override public void send(String recipient, String message) { System.out.println("[DEV] Would send to " + recipient + ": " + message); } }

The ! prefix negates the condition. You can also use the Spring Expression Language (SpEL) form @Profile({"prod", "staging"}) to activate a bean when either profile is active, or @Profile("prod & cloud") to require both.

Profile Groups (Spring Boot 2.4+)

Starting with Spring Boot 2.4, you can define profile groups — a single high-level name that expands to several concrete profiles. This avoids forcing callers to remember a long list of comma-separated profile names.

# application.properties spring.profiles.group.production=prod,monitoring,cloud-aws spring.profiles.group.local=dev,seed-data

Now launching with SPRING_PROFILES_ACTIVE=production automatically activates prod, monitoring, and cloud-aws together. Your CI/CD pipeline stays clean, and each sub-profile can focus on a single concern.

The Default Profile

If no profile is activated, Spring uses the built-in default profile. Any bean annotated with @Profile("default") is only registered when nothing else is active. This is a convenient safety net for ensuring at least some beans are present during quick ad-hoc runs, but in practice most teams always set an explicit active profile.

Watch out for profile-less beans accidentally running in production. If you define a bean without @Profile, it loads in every environment — including production. A development-only data-seeding bean that lacks @Profile("dev") could silently insert test data in production on the next deploy.

Inspecting the Active Profile at Runtime

You can inject the currently active profiles programmatically via the Environment abstraction:

import org.springframework.core.env.Environment; import org.springframework.stereotype.Component; @Component public class ProfileReporter { private final Environment env; public ProfileReporter(Environment env) { this.env = env; } public void report() { String[] active = env.getActiveProfiles(); System.out.println("Active profiles: " + String.join(", ", active)); } }

This is especially useful in startup log messages or admin endpoints to make it immediately obvious which environment a running instance belongs to.

Summary

Spring Profiles let you ship a single artifact and adapt its behaviour per environment without touching code. Name your profiles clearly (dev, test, staging, prod), keep credentials out of all profile files (delegate them to environment variables), and activate the right profile from the deployment pipeline rather than from committed source. Use @Profile on beans for implementation swapping, and profile groups to compose complex environments from focused building blocks. In the next lesson you will see how YAML provides a more structured and concise alternative to properties files, including multi-document YAML for keeping all profiles in one file.