Spring Boot Profiles & Environments: @Profile, Environment-Specific Config
Manage environment-specific configuration in Spring Boot using profiles, @Profile annotation, and property file naming conventions.
Manage environment-specific configuration in Spring Boot using profiles, @Profile annotation, and property file naming conventions. The guide uses practical examples to explain spring boot profiles & environments, introduction to spring profiles and shows how to apply the ideas in a Spring Boot project. It closes with common pitfalls and production checks so you can apply the pattern with fewer surprises.
Spring Boot Profiles & Environments
Introduction to Spring Profiles
Spring Profiles let you split your application configuration by environment. Instead of juggling multiple configuration files or sprinkling if (environment.equals(“prod”)) throughout your code, you activate a profile and Spring loads the right beans and properties automatically.
Your app runs in different contexts: dev, test, staging, production. Each context needs different configuration. Dev probably wants a local database and verbose logging. Production needs a managed database and minimal output. Profiles handle this cleanly—you ship one artifact and control behavior through external configuration.
Spring checks which profile is active and loads only the configuration that belongs to it. Same deployable artifact, different behavior per environment.
Activating Profiles
Using spring.profiles.active
The most common way to flip profiles is through the spring.profiles.active property. Several ways to set it:
In application.properties or application.yml:
spring.profiles.active=dev
spring:
profiles:
active: dev
Environment variable:
export SPRING_PROFILES_ACTIVE=dev,metrics
Command-line:
java -jar myapp.jar --spring.profiles.active=prod
JNDI (if you happen to be in that world):
<Environment name="spring.profiles.active" value="prod" type="java.lang.String"/>
You can activate multiple profiles at once. When profiles conflict, later ones win.
Using @Profile Annotation
The @Profile annotation marks a bean or configuration class as profile-specific. The bean only gets created when that profile is active.
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
@Component
@Profile("dev")
public class DevDataSource implements DataSource {
// Development database configuration
}
You can also negate and combine profiles:
@Profile("!prod") // Activates when NOT prod
@Profile("dev & !test") // dev AND NOT test
@Profile({"dev", "local"}) // dev OR local
Profile-Specific Configuration Files
Spring Boot automatically picks up files named application-{profile}.yml or application-{profile}.properties.
File Hierarchy
src/
├── main/
│ ├── resources/
│ │ ├── application.yml # Shared defaults
│ │ ├── application-dev.yml # Dev overrides
│ │ ├── application-test.yml # Test overrides
│ │ ├── application-staging.yml # Staging overrides
│ │ └── application-prod.yml # Production overrides
Spring merges the base file with the profile-specific file. Profile-specific values override base values.
Example Configuration Files
application.yml (base):
spring:
application:
name: myapp
datasource:
url: jdbc:postgresql://localhost:5432/mydb
driver-class-name: org.postgresql.Driver
logging:
level:
root: INFO
application-dev.yml:
spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb_dev
username: devuser
password: devpass
logging:
level:
root: DEBUG
com.myapp: TRACE
application-prod.yml:
spring:
datasource:
url: jdbc:postgresql://prod-db.internal:5432/mydb
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: 20
minimum-idle: 5
logging:
level:
root: WARN
com.myapp: INFO
@Profile for Conditional Bean Registration
Beyond properties, @Profile controls which beans Spring creates. This is where things get interesting.
Real-World Example
public interface CacheManager {
void put(String key, Object value);
Object get(String key);
}
@Component
@Profile("dev")
public class InMemoryCacheManager implements CacheManager {
private final Map<String, Object> cache = new ConcurrentHashMap<>();
@Override
public void put(String key, Object value) {
cache.put(key, value);
}
@Override
public Object get(String key) {
return cache.get(key);
}
}
@Component
@Profile("prod")
public class RedisCacheManager implements CacheManager {
private final RedisTemplate<String, Object> redisTemplate;
@Override
public void put(String key, Object value) {
redisTemplate.opsForValue().set(key, value, Duration.ofHours(24));
}
@Override
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
}
Depending on the active profile, your application gets either an in-memory cache or a Redis-backed one. No interface changes, no code changes. Just flip the profile.
Configuration Classes with @Profile
You can annotate entire @Configuration classes too:
@Configuration
@Profile("prod")
public class ProductionSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain() {
return SecurityFilterChainBuilder
.create()
.withOAuth2()
.withRateLimiting()
.build();
}
}
The entire security configuration only loads in production. Dev never accidentally picks up production security rules.
Programmatic Profile Activation
Sometimes you need to set profiles in code rather than through configuration files.
Setting Active Profiles at Startup
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
public class MyApplication {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(MyApplication.class);
app.setAdditionalProfiles("dev", "local");
app.run(args);
}
}
Dynamically Activating Profiles
If you need custom logic, listen for the ApplicationEnvironmentPreparedEvent:
import org.springframework.context.annotation.Configuration;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.context.ApplicationListener;
@Configuration
public class ProfileActivationListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {
@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
Environment env = event.getEnvironment();
String hostname = env.getProperty("HOSTNAME", "unknown");
if (hostname.startsWith("prod-")) {
env.addActiveProfile("prod");
} else if (hostname.startsWith("staging-")) {
env.addActiveProfile("staging");
}
}
}
This lets you auto-detect the environment based on hostname. Useful when you do not want to pass profile flags manually.
Profile Activation Order
Spring applies profiles in a specific order. Later profiles override earlier ones:
- Default profile (
default) spring.profiles.defaultpropertyspring.profiles.activeproperty- Command-line arguments
- Programmatic
setAdditionalProfiles()calls
Profile-Specific Property Sources
Profiles work with Spring’s property source system beyond just YAML files.
Environment Variables
Spring maps environment variables to properties using a predictable naming convention:
# Sets datasource.url for the dev profile
SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/devdb
# Sets datasource.url for the prod profile
SPRING_PROD_DATASOURCE_URL=jdbc:postgresql://prod:5432/proddb
System Properties
java -Dspring.profiles.active=prod -Dserver.port=8443 -jar app.jar
Custom Property Sources
You can register property sources tied to a profile:
@Configuration
@Profile("cloud")
public class CloudConfigPropertiesConfig {
@Bean
public PropertySource<?> cloudPropertySource() {
return new MapPropertySource("cloud", Map.of(
"cloud.provider", "aws",
"cloud.region", "us-east-1"
));
}
}
When to Use / When NOT to Use
When to Use Profiles
- Environment-specific database connections (dev vs prod)
- Different bean implementations per environment
- Logging levels that vary by deployment
- External service endpoints that change across environments
- Security configurations that differ by context
- Feature flags tied to deployment context
When NOT to Use Profiles
- Business logic variation across tenants: Profiles are not a multi-tenancy solution. Use proper tenancy architecture.
- A/B testing feature toggles: Use a dedicated feature toggle system.
- Runtime configuration changes: Profiles are fixed at startup. For dynamic config, look at Spring Cloud Config or Consul.
- User-specific variation: Profiles are deployment-level, not user-level.
- Constants that never change: If the value is the same everywhere, just hardcode it.
Common Pitfalls
1. Property Override Behavior
Spring merges properties from multiple sources in a set order. If a property is not overriding as expected, the property source ordering is probably the culprit. Debug it:
java -jar app.jar --debug | grep "PropertySource"
2. Missing Default Profile
No profile specified means Spring activates the default profile. If your base application.yml does not have the values you expect, things will silently fail or use wrong defaults.
3. Multiple Profiles with Conflicting Properties
spring.profiles.active=dev,aws # aws overrides dev
Know your precedence when stacking profiles.
4. @Profile on Interface Implementation
If an interface itself has profile restrictions and no implementation matches the active profile, dependency injection will fail at startup. The interface should be unrestricted, with @Profile only on the implementations.
5. Test Profiles
Tests that do not specify a profile may pick up production configuration accidentally. Always be explicit:
@SpringBootTest
@ActiveProfiles("test")
class MyServiceTest {
// ...
}
Security Notes
Sensitive Properties
Never commit secrets to profile-specific files. Use environment variable placeholders:
# application-prod.yml
spring:
datasource:
password: ${DB_PASSWORD}
Spring resolves ${DB_PASSWORD} from the environment at runtime. The actual password never touches version control.
Secret Scanning
Run pre-commit hooks to catch secrets before they enter your repository:
git secrets --install
git secrets --scan
Production Profile Lockdown
You can enforce explicit profile activation in production to prevent accidental dev configuration:
@Configuration
@Profile("prod")
public class ProductionProfileLock {
@PostConstruct
public void verifyProductionSetup() {
if (System.getProperty("spring.profiles.active") == null) {
throw new IllegalStateException(
"Production profile must be explicitly activated"
);
}
}
}
This fails fast if someone deploys to production without the right profile flag.
Implementation Snippets
Multi-Profile Activation with Maven
<profiles>
<profile>
<id>dev</id>
<activation>
<property>
<name>env</name>
<value>dev</value>
</property>
</activation>
<properties>
<spring.profiles.active>dev</spring.profiles.active>
</properties>
</profile>
<profile>
<id>prod</id>
<activation>
<property>
<name>env</name>
<value>prod</value>
</property>
</activation>
<properties>
<spring.profiles.active>prod</spring.profiles.active>
</properties>
</profile>
</profiles>
Profile-Specific Bean Conditions
@Configuration
public class DataSourceConfig {
@Bean
@Profile("dev")
public DataSource devDataSource() {
return DataSourceBuilder.create()
.url("jdbc:h2:mem:devdb")
.driverClassName("org.h2.Driver")
.build();
}
@Bean
@Profile("prod")
public DataSource prodDataSource() {
return DataSourceBuilder.create()
.url(System.getenv("DB_URL"))
.driverClassName("org.postgresql.Driver")
.username(System.getenv("DB_USER"))
.password(System.getenv("DB_PASSWORD"))
.build();
}
}
Conditional Configuration with @ConditionalOnProperty
@Configuration
@Profile("aws")
@ConditionalOnProperty(name = "aws.s3.enabled", havingValue = "true", matchIfMissing = true)
public class S3Config {
@Bean
public AmazonS3 amazonS3(AWSProperties properties) {
return AmazonS3ClientBuilder.standard()
.withRegion(properties.getRegion())
.build();
}
}
Observability Checklist
When using profiles in production, make sure you can see what profile is running:
- Profile Display: Log the active profile at startup
- Metrics Tagged by Profile: Add profile as a metric dimension
- Logging Profile Context: Include profile name in correlation IDs
- Configuration Validation: Check required properties exist for each profile
- Startup Warnings: Alert when an unexpected profile activates
- Health Checks: Verify profile-specific dependencies are reachable
Startup Profile Logging
# application.yml
spring:
main:
show-banner: true
logging:
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%profile] %-5level %logger{36} - %msg%n"
Actuator Profile Endpoint
management:
endpoints:
web:
exposure:
include: health,info,env,beans
endpoint:
health:
show-details: always
Hit /actuator/env to see every property source and its values for the active profile. Useful when debugging why a property has an unexpected value.
Trade-off Table
| Aspect | Using Profiles | Alternative Approaches |
|---|---|---|
| Complexity | Low to medium | Feature toggles (high), multi-tenancy (medium) |
| Startup time | Minimal impact | Conditional beans add slight overhead |
| Maintenance | Single codebase | Multiple branches or deployments |
| Flexibility | Fixed at startup | Runtime config (Spring Cloud Config) |
| Testing | Easy to mock | Requires profile switching |
| Documentation | Implicit behavior | Explicit toggle docs needed |
| Secret Management | External required | Centralized secret service |
Quick Recap Checklist
- Understand how profiles segregate configuration
- Know multiple ways to activate profiles
- Use
application-{profile}.ymlfor environment-specific properties - Apply
@Profilefor conditional bean registration - Understand property precedence across profiles
- Activate multiple profiles when needed
- Set up test profiles with
@ActiveProfiles - Protect sensitive values with environment variables
- Validate profile activation in production
- Log active profile at startup for debugging
- Document which profile each environment uses
- Use feature flags for runtime toggles, not profiles
Failure Scenarios
Scenario 1: Profile Not Activated in Production
Problem: Application starts with the default profile instead of prod. Wrong database, exposed dev endpoints.
Fix: Fail fast at startup:
@Configuration
@Profile("prod")
public class ProductionProfileValidator {
@Autowired
private ConfigurableEnvironment env;
@PostConstruct
public void validate() {
if (!env.getActiveProfiles().contains("prod")) {
throw new IllegalStateException("Attempted to run in production without prod profile");
}
}
}
Scenario 2: Missing Profile-Specific Property
Problem: Code references a property that only exists in one profile. Null values or exceptions follow.
Fix: Always provide defaults in application.yml. Use @Value with fallback values:
@Value("${cache.ttl:3600}")
private int cacheTtl;
Scenario 3: Circular Profile Dependencies
Problem: A bean with @Profile(“redis”) depends on another bean that is also profile-gated and might not be active.
Fix: Create a no-op default implementation:
@Component
public class DefaultCacheManager implements CacheManager {
// Does nothing or uses a simple in-memory fallback
}
@Component
@Profile("redis")
public class RedisCacheManager implements CacheManager {
// Real Redis implementation
}
The default is always present. The Redis profile overrides it when active.
Interview Questions
Spring Boot evaluates property sources in a fixed order: command-line arguments first, then environment variables, then application properties from profile-specific files, then defaults. When you activate multiple profiles like spring.profiles.active=dev,prod, the prod profile's properties override dev's for any conflicting keys. The order you specify matters. You can see the exact resolution order by running with --debug and checking the PropertySource output, or by inspecting ConfigurableEnvironment programmatically in code.
Yes. The annotation uses SpEL under the hood, so you get negation with !, like @Profile("!prod") to activate when prod is not active. For AND logic, use & as in @Profile("dev & !test"), which reads as "dev AND NOT test." For OR logic, use an array: @Profile({"dev", "local"}) activates when either profile is active. If your expression gets complex, remember that & needs escaping as & inside an annotation string.
Spring Boot automatically activates the default profile. That means it looks for application-default.yml or application-default.properties if they exist—though most projects do not bother creating them. Your base application.yml loads regardless. You can change the default profile by setting spring.profiles.default. In production, always explicitly activate a profile rather than relying on defaults. You want to know exactly which configuration is running.
Never hardcode secrets in files that go into version control. Use placeholders like ${DB_PASSWORD} and let Spring resolve them from environment variables at runtime. For enterprise deployments, integrate with HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault—these can inject secrets as environment variables during deployment so secrets never appear in config files. Run secret scanning tools on pre-commit to catch accidental credential commits before they reach your repository.
@Profile gates beans based on which deployment profile is active—useful for swapping entire implementations between dev and prod, like using an in-memory cache versus Redis. @ConditionalOnProperty is broader: it gates beans based on any property value, not just the active profile. You would use it for feature flags, optional integrations, or configuration-driven behavior that might change independently of the deployment environment. They can be combined: @Profile("aws") @ConditionalOnProperty(name = "s3.enabled") only activates S3 beans when you are on AWS and explicitly enable S3.
Further Reading
- Spring Boot Externalized Configuration - Official documentation on how Spring Boot resolves property values from different sources
- Spring Profile Documentation - Core Spring profiles documentation for conditional bean creation
- Property Source Abstraction - Deep dive into Spring’s property source hierarchy and precedence
- Spring Boot Actuator Endpoints - Monitor and inspect application state including profile configuration
- Environment Variables Mapping - How Spring Boot maps environment variables to configuration properties
- Vault Integration with Spring Cloud Config - Enterprise secret management integration patterns
- 12-Factor App: Config - Best practices for managing configuration across environments
- Testing Spring Boot Applications - Using
@ActiveProfilesand test-specific profile configuration
Conclusion
Spring Profiles provide a clean mechanism for environment-specific configuration without resorting to multiple artifacts or conditional logic scattered throughout your codebase. The key is using them thoughtfully: activate profiles through environment variables for deployment flexibility, use @Profile annotations for conditional bean registration, and always protect sensitive values with environment variable placeholders rather than hardcoding secrets.
Profile-based configuration works best when combined with external configuration management for secrets and centralized configuration servers for runtime changes. Use profiles as the coarse-grained mechanism for environment differences (dev vs prod), and feature flags for fine-grained runtime toggles that may change independently of the deployment environment.
Category
Related Posts
Spring Boot Build Tools: Maven & Gradle
Configure Maven and Gradle for Spring Boot projects—plugins, dependency management, packaging JARs and WARs, and build automation essentials.
Embedded Web Servers in Spring Boot: Tomcat, Jetty, Undertow
Configure embedded servers in Spring Boot: compare Tomcat, Jetty, and Undertow, tune thread pools, enable access logs, and switch implementations.
JUnit 5 & Jupiter: Lifecycle, Nested & Parameterized Tests
Explore JUnit 5 Jupiter features: master test lifecycle annotations, organize tests with @Nested, and parameterize tests with @CsvSource and @MethodSource.