Spring Boot Profiles: dev, test, prod Configuration
1. Overview
Your application typically runs in multiple environments. Your machine uses H2 database. The test server uses a separate PostgreSQL. Production uses a real cluster with real SMTP. If you manually edit the configuration file before every deploy, sooner or later you’ll accidentally push dev configuration to prod.
Spring Boot solves this with profiles. After reading this article, you’ll know how to split configuration files by profile, enable/disable beans using @Profile, activate profiles in various ways, write tests with @ActiveProfiles, and avoid common mistakes.
2. What is a Profile?
A profile is a named label, like dev, test, or prod. Spring uses this label to decide two things:
- Which configuration is loaded:
application-dev.ymlis only read when thedevprofile is active. - Which beans are created: a bean with
@Profile("prod")is only registered when theprodprofile is active.
If no profile is explicitly activated, Spring uses the default profile named default. Beans without @Profile are always created, regardless of which profile is active.
You only need the basic web dependency for the examples in this article:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
3. Configuration Files by Profile
3.1. Splitting into Multiple Files
The naming convention is application-{profile}.yml (or .properties). The application.yml file contains common configuration and is always loaded:
# application.yml
spring:
application:
name: shop-service
profiles:
default: dev
app:
mail:
from: no-reply@shop.vn
host: localhost
Profile-specific files only contain the differences:
# application-dev.yml
server:
port: 8080
logging:
level:
root: DEBUG
# application-test.yml
server:
port: 0
app:
mail:
host: fake-smtp
# application-prod.yml
server:
port: 80
logging:
level:
root: WARN
app:
mail:
host: ${MAIL_HOST}
Explanation:
spring.profiles.default: devmeans when you don’t activate any profile, Spring usesdevinstead ofdefault. Your dev machine runs immediately without needing parameters.- When the
prodprofile is active, Spring loadsapplication.ymlfirst, thenapplication-prod.yml. Values in the profile-specific file override the common values. Soapp.mail.hostbecomes the value of theMAIL_HOSTenvironment variable, whileapp.mail.fromremainsno-reply@shop.vn. server.port: 0intestlets Spring choose a random port, avoiding conflicts when running tests in parallel.
3.2. Merging into a Single YAML File
If you want fewer files, you can use YAML multi-document syntax, separated by ---:
# application.yml
app:
mail:
host: localhost
---
spring:
config:
activate:
on-profile: prod
app:
mail:
host: smtp.shop.vn
The second document only applies when prod is active. This is concise when each profile has few differences. For longer configurations, separate files are still more readable and easier to review.
Note: In
.propertiesfiles, use#---to separate documents instead of---.
4. Activating Profiles
There are many ways to do this. The order of precedence follows Spring Boot’s general rules: command-line arguments > environment variables > configuration files.
Command-line argument:
java -jar shop-service.jar --spring.profiles.active=prod
JVM system property:
java -Dspring.profiles.active=prod -jar shop-service.jar
Environment variable, the most common approach on Docker and Kubernetes:
export SPRING_PROFILES_ACTIVE=prod
java -jar shop-service.jar
Using the Maven plugin:
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev
Programmatically, useful when you always want to add a certain profile:
@SpringBootApplication
public class ShopApplication {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(ShopApplication.class);
app.setAdditionalProfiles("metrics");
app.run(args);
}
}
setAdditionalProfiles adds a profile to the list, it doesn’t replace profiles activated from the outside. Running with --spring.profiles.active=prod means both prod and metrics will be active.
You can activate multiple profiles at once using a comma: --spring.profiles.active=prod,eu. If two files define the same key, the later profile wins. In this example, application-eu.yml overrides application-prod.yml.
When the application starts, the log tells you which profile is active:
INFO c.e.shop.ShopApplication : The following 1 profile is active: "prod"
If no profile is activated and spring.profiles.default is not set, you’ll see:
INFO c.e.shop.ShopApplication : No active profile set, falling back to 1 default profile: "default"
Always glance at this log line after deploying. It’s the quickest way to catch a server running the wrong profile.
5. Enabling/Disabling Beans with @Profile
Different configuration is only half the battle. Sometimes you need a completely different implementation. For example: dev just prints emails to console, test stores them in memory for verification, prod sends them via SMTP.
Define a common interface:
public interface NotificationService {
void send(String to, String message);
}
Implementation for dev:
@Service
@Profile("dev")
public class ConsoleNotificationService implements NotificationService {
@Override
public void send(String to, String message) {
System.out.println("[DEV] Sending to " + to + ": " + message);
}
}
Implementation for test, storing messages in memory for assertion:
@Service
@Profile("test")
public class InMemoryNotificationService implements NotificationService {
private final List<String> sentMessages = new CopyOnWriteArrayList<>();
@Override
public void send(String to, String message) {
sentMessages.add(to + "|" + message);
}
public List<String> getSentMessages() {
return List.copyOf(sentMessages);
}
}
Implementation for prod:
@Service
@Profile("prod")
public class SmtpNotificationService implements NotificationService {
private final String host;
public SmtpNotificationService(@Value("${app.mail.host}") String host) {
this.host = host;
}
@Override
public void send(String to, String message) {
// In real code, use JavaMailSender; here we're just demonstrating
System.out.println("[SMTP " + host + "] Sending to " + to);
}
}
The class that uses it doesn’t need to know which environment it’s in:
@Service
public class OrderService {
private final NotificationService notificationService;
public OrderService(NotificationService notificationService) {
this.notificationService = notificationService;
}
public void placeOrder(String customerEmail, long orderId) {
notificationService.send(customerEmail, "Order #" + orderId + " has been created");
}
}
At any given time, only one of the three NotificationService beans is created. Spring injects the correct one into OrderService. Running with the dev profile and calling placeOrder("an@shop.vn", 42), you get:
[DEV] Sending to an@shop.vn: Order #42 has been created
5.1. Profile Expressions
@Profile supports logical expressions:
@Configuration
public class SecurityConfig {
@Bean
@Profile("!prod")
public CommandLineRunner seedDemoData() {
return args -> System.out.println("Loading demo data...");
}
@Bean
@Profile("dev | test")
public CommandLineRunner relaxedCors() {
return args -> System.out.println("CORS open to all origins");
}
}
!prod: create the bean in every profile exceptprod.dev | test: create whendevortestis active. There’s also&for “and”, for exampleprod & eu.
@Profile can be placed on @Component/@Configuration classes or on individual @Bean methods. Placing it on @Configuration applies the condition to all beans inside.
5.2. Checking the Profile in Code
Sometimes you need to branch in your logic. Use Environment:
@Component
public class StartupBanner implements CommandLineRunner {
private final Environment environment;
public StartupBanner(Environment environment) {
this.environment = environment;
}
@Override
public void run(String... args) {
if (environment.acceptsProfiles(Profiles.of("prod"))) {
System.out.println("Running PRODUCTION");
}
}
}
Use this approach sparingly. If your code is full of if (prod) checks, you should be creating separate beans with @Profile instead.
6. Profile Groups
As your system grows, you might want to split configuration further, like proddb for database and prodmq for the message broker. Profile groups let you activate a cluster of profiles with one name:
# application.yml
spring:
profiles:
group:
prod: proddb, prodmq
dev: devdb
Now running --spring.profiles.active=prod activates prod, proddb, and prodmq. Spring loads application-prod.yml, application-proddb.yml, and application-prodmq.yml in order. Operators only need to remember one name.
7. Writing Tests with @ActiveProfiles
In tests, you select a profile using @ActiveProfiles. The test below verifies two things: the correct bean is injected, and configuration from application-test.yml takes effect.
@SpringBootTest
@ActiveProfiles("test")
class OrderServiceProfileTest {
@Autowired
private OrderService orderService;
@Autowired
private NotificationService notificationService;
@Value("${app.mail.host}")
private String mailHost;
@Test
void shouldUseInMemoryNotificationInTestProfile() {
assertThat(notificationService).isInstanceOf(InMemoryNotificationService.class);
}
@Test
void shouldRecordNotificationWhenOrderPlaced() {
orderService.placeOrder("binh@shop.vn", 7);
var inMemory = (InMemoryNotificationService) notificationService;
assertThat(inMemory.getSentMessages())
.contains("binh@shop.vn|Order #7 has been created");
}
@Test
void shouldOverrideMailHostFromTestProfile() {
assertThat(mailHost).isEqualTo("fake-smtp");
}
}
Explanation:
@ActiveProfiles("test")activates thetestprofile for this class’s context, regardless of whatspring.profiles.defaultis.- The first test confirms that Spring chose
InMemoryNotificationServiceinstead of sending real emails. - The second test uses the in-memory bean to verify the message content without needing mocks.
- The third test proves that
app.mail.hostfromapplication-test.ymlhas overridden thelocalhostvalue inapplication.yml.
Note: Each different combination of profiles creates a separate Spring context. If each test class uses a different set of profiles, the context cache can’t be reused and your test suite runs significantly slower. Keep the number of profile combinations in your tests to a minimum.
8. Common Pitfalls
Setting spring.profiles.active in a profile-specific file. Starting with Spring Boot 2.4, declaring spring.profiles.active or spring.profiles.include in application-dev.yml (or in a document with on-profile) will prevent your application from starting:
InvalidConfigDataPropertyException: Property 'spring.profiles.active' imported from
location 'class path resource [application-dev.yml]' is invalid in a profile specific resource
Use profile groups (section 6) instead.
Hard-coding spring.profiles.active=dev in application.yml. This file is packaged inside the jar. If someone forgets to pass the environment variable on the server, prod will run with dev configuration. Use spring.profiles.default for your local environment, and always activate the profile explicitly on the server.
Storing secrets in application-prod.yml. Database passwords and API keys should not go in Git. Use placeholders like ${DB_PASSWORD} and provide the value via environment variables or a secrets manager.
No bean matches the profile. If you run with the staging profile but only have implementations for dev, test, prod, the application will report an error:
Parameter 0 of constructor in OrderService required a bean of type
'NotificationService' that could not be found.
Consider adding a fallback bean with @Profile("!prod") or add an implementation for the new profile.
Abusing profiles for feature flags. Profiles are good for describing environments. If you want to enable/disable a feature independently of the environment, @ConditionalOnProperty is often clearer:
@Bean
@ConditionalOnProperty(name = "app.feature.loyalty-points", havingValue = "true")
public LoyaltyService loyaltyService() {
return new LoyaltyService();
}
When you end up with profiles like prod-with-loyalty or dev-no-cache, that’s a sign you should switch to properties instead.
9. Conclusion
Profiles let you separate configuration and behavior by environment without changing code. You’ve seen how to use application-{profile}.yml to override configuration, @Profile to select implementations, profile groups to bundle multiple profiles, and @ActiveProfiles for testing.
A few principles to remember: put common configuration in application.yml, only write the differences in profile-specific files. Activate profiles on the server via environment variables or command-line arguments. Don’t put secrets in Git. And always check the log line The following profiles are active after each deployment.