Profiles
Switch beans and configuration on per environment with @Profile and spring.profiles.active, without turning profiles into scattered feature flags.
@Profile("prod") | @Profile("!prod") | @Profile("prod & eu")
spring.profiles.active=prod (or SPRING_PROFILES_ACTIVE=prod)A profile is a named label, such as dev, test or prod, that you activate when the application starts. Beans and configuration files can be tied to a profile, so the same build behaves differently in each environment without a code change.
Tying beans to a profile
Put @Profile on a @Component class or a @Bean method. The bean is registered only when the expression matches the active profiles. Expressions support !, & and |, so @Profile("!prod") means every environment except production and @Profile("prod & eu") needs both.
@Profile("prod") class SmtpEmailSender implements EmailSender { ... }
@Profile("!prod") class LoggingEmailSender implements EmailSender { ... }Pairing a profile with its negation guarantees exactly one implementation, whatever is active.
Activating profiles
Set spring.profiles.active outside the jar, so one artefact runs everywhere: the SPRING_PROFILES_ACTIVE environment variable in containers, --spring.profiles.active=prod on the command line, or @ActiveProfiles in tests. With nothing active, Spring uses a profile called default.
spring.profiles.group lets one name switch on several: activating prod can also activate postgres and json-logs. Profiles also select configuration files such as application-prod.yaml, which the configuration lessons cover in detail.
Mistakes that bite in production
- A typo in the active profile is not an error.
SPRING_PROFILES_ACTIVE=prdstarts the application with none of your production beans or settings. Log the active profiles at startup and check them in your deployment. - Setting the active profile inside a profile-specific file.
spring.profiles.activeinapplication-prod.propertiesis rejected at startup; set it from outside. - Too many profiles. A profile per customer or per feature produces combinations nobody has tested. Use profiles for environments and properties for switches, ideally with
@ConditionalOnPropertyor@ConfigurationProperties. - Checking profiles in business code.
if (env.matchesProfiles("prod"))scattered through services is hard to test. Put the difference into two beans and let the container choose. - Dev beans leaking into production. A data seeder marked
@Profile("dev")is safe; one with no profile runs everywhere. Be explicit for anything that writes data.
Decide which way a missing profile should fail, and make it visible. In the example below a test server never sends real email, but a production server started without prod would silently log emails instead of sending them. A startup check that refuses to boot when none of the expected profiles is active closes that gap.
Example
import org.springframework.context.annotation.Profile;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Component;
interface EmailSender {
void send(String to, String subject, String body);
}
// Production: real SMTP through Spring Boot's auto-configured JavaMailSender.
("prod")
class SmtpEmailSender implements EmailSender {
private final JavaMailSender mail;
SmtpEmailSender(JavaMailSender mail) {
this.mail = mail;
}
public void send(String to, String subject, String body) {
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(to);
message.setSubject(subject);
message.setText(body);
mail.send(message);
}
}
// Everywhere else: write to the log so nobody receives test emails.
("!prod")
public class LoggingEmailSender implements EmailSender {
public void send(String to, String subject, String body) {
System.out.println("[email not sent] to=" + to + " subject=" + subject);
}
}
When to use it
- Log emails locally in development and send them over SMTP only in production.
- Switch on the Postgres and JSON-logging set-up together by activating a single prod profile.
- Seed demonstration data in the dev profile without any risk of it running in production.
More examples
Profile groups and defaults
Groups let deployment set one name while the code keeps small, focused profiles: postgres only configures the database, json-logs only the log format. spring.profiles.default applies when nothing is activated explicitly, which makes a developer's plain run behave like dev without anyone remembering a flag.
# application.properties
# With nothing set, run as "dev" rather than the built-in "default" profile.
spring.profiles.default=dev
# Activating "prod" also activates "postgres" and "json-logs".
spring.profiles.group.prod=postgres,json-logs
spring.profiles.group.dev=h2
# Plain settings, with a property rather than a profile for the feature switch.
app.audit.enabled=trueActivating a profile when the application starts
All three set the same property, from different sources. Prefer the environment variable in Docker and Kubernetes, so the image stays identical and the environment decides. Check the startup log line that lists the active profiles; it is the fastest way to catch a typo.
# Command line argument
java -jar target/shop.jar --spring.profiles.active=prod
# Environment variable, the usual choice in containers
SPRING_PROFILES_ACTIVE=prod java -jar target/shop.jar
# Maven during development
./mvnw spring-boot:run -Dspring-boot.run.profiles=devTesting that each profile gets the right bean
The runner builds a small context per test, so both branches are checked in milliseconds. The Environment reads spring.profiles.active from the property values when the profile condition is evaluated. In a full @SpringBootTest, @ActiveProfiles("prod") does the same job.
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
class StorageProfileTest {
private final ApplicationContextRunner runner = new ApplicationContextRunner()
.withUserConfiguration(StorageConfig.class);
@Test
void usesLocalDiskWithoutProd() {
runner.run(context -> assertThat(context.getBean(FileStore.class)).isInstanceOf(LocalFileStore.class));
}
@Test
void usesObjectStorageInProd() {
runner.withPropertyValues("spring.profiles.active=prod")
.run(context -> assertThat(context.getBean(FileStore.class)).isInstanceOf(ObjectFileStore.class));
}
@Configuration(proxyBeanMethods = false)
static class StorageConfig {
@Bean
@Profile("prod")
FileStore objectFileStore() {
return new ObjectFileStore();
}
@Bean
@Profile("!prod")
FileStore localFileStore() {
return new LocalFileStore();
}
}
interface FileStore {}
static class ObjectFileStore implements FileStore {}
static class LocalFileStore implements FileStore {}
}
Discussion