Overview
Profile-specific properties allow you to configure your application differently for various environments (development, testing, production, etc.) without code changes. Spring Boot provides robust support for environment-specific configuration.
Core Concepts
- Profiles: Named groups of configuration (dev, test, prod, etc.)
- Property Sources: Hierarchical property loading
- Profile Activation: Ways to enable specific profiles
- Property Overrides: Order of precedence for properties
Basic Configuration
1. Profile-Specific Property Files
src/main/resources/ ├── application.properties # Default properties ├── application-dev.properties # Development profile ├── application-test.properties # Test profile ├── application-prod.properties # Production profile └── application-local.properties # Local development
2. Property File Examples
application.properties (default):
# Default configuration app.name=MyApplication app.version=1.0.0 server.port=8080 spring.datasource.url=jdbc:h2:mem:defaultdb spring.jpa.hibernate.ddl-auto=create-drop logging.level.root=INFO # Profile activation (can be overridden externally) spring.profiles.active=dev
application-dev.properties:
# Development specific configuration server.port=8080 spring.datasource.url=jdbc:h2:mem:devdb spring.datasource.username=devuser spring.datasource.password=devpass spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true logging.level.com.myapp=DEBUG logging.level.org.hibernate.SQL=DEBUG # Feature flags app.features.cache-enabled=false app.features.email-enabled=false app.features.analytics-enabled=false # External services (development endpoints) app.services.payment.url=http://localhost:8081/payment app.services.notification.url=http://localhost:8082/notification
application-test.properties:
# Test environment configuration server.port=0 # Random port for tests spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1 spring.datasource.username=testuser spring.datasource.password=testpass spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.show-sql=false # Test-specific settings spring.main.lazy-initialization=true spring.jpa.defer-datasource-initialization=true # Feature flags for testing app.features.cache-enabled=true app.features.email-enabled=false # Don't send emails in tests app.features.analytics-enabled=false # Test endpoints app.services.payment.url=http://test-payment:8081 app.services.notification.url=http://test-notification:8082
application-prod.properties:
# Production configuration
server.port=8080
server.compression.enabled=true
server.servlet.session.timeout=30m
# Production database
spring.datasource.url=${DB_URL:jdbc:postgresql://localhost:5432/proddb}
spring.datasource.username=${DB_USERNAME:produser}
spring.datasource.password=${DB_PASSWORD:prodpass}
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=false
spring.jpa.properties.hibernate.format_sql=false
# Production logging
logging.level.root=WARN
logging.level.com.myapp=INFO
logging.file.name=/var/log/myapp/application.log
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n
# Production features
app.features.cache-enabled=true
app.features.email-enabled=true
app.features.analytics-enabled=true
# Production services
app.services.payment.url=https://payment.myapp.com
app.services.notification.url=https://notification.myapp.com
# Security
app.security.require-ssl=true
app.security.cors.allowed-origins=https://myapp.com
# Monitoring
management.endpoints.web.exposure.include=health,metrics,info
management.endpoint.health.show-details=when_authorized
YAML Configuration
1. YAML Profile Configuration
application.yml:
# Default configuration
app:
name: MyApplication
version: 1.0.0
security:
jwt-secret: default-secret-key
token-expiration: 86400000
server:
port: 8080
spring:
datasource:
url: jdbc:h2:mem:defaultdb
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create-drop
show-sql: false
profiles:
active: dev
logging:
level:
root: INFO
com.myapp: DEBUG
---
# Development profile
spring:
config:
activate:
on-profile: dev
server:
port: 8080
spring:
datasource:
url: jdbc:h2:mem:devdb
username: devuser
password: devpass
jpa:
show-sql: true
hibernate:
ddl-auto: update
properties:
hibernate:
format_sql: true
logging:
level:
com.myapp: DEBUG
org.hibernate.SQL: DEBUG
org.hibernate.type: TRACE
app:
features:
cache-enabled: false
email-enabled: false
analytics-enabled: false
services:
payment:
url: http://localhost:8081/payment
timeout: 5000
notification:
url: http://localhost:8082/notification
timeout: 3000
---
# Test profile
spring:
config:
activate:
on-profile: test
server:
port: 0 # Random port
spring:
datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
username: testuser
password: testpass
jpa:
hibernate:
ddl-auto: create-drop
show-sql: false
main:
lazy-initialization: true
app:
features:
cache-enabled: true
email-enabled: false
analytics-enabled: false
services:
payment:
url: http://test-payment:8081
timeout: 2000
notification:
url: http://test-notification:8082
timeout: 2000
---
# Production profile
spring:
config:
activate:
on-profile: prod
server:
port: 8080
compression:
enabled: true
servlet:
session:
timeout: 30m
spring:
datasource:
url: ${DB_URL:jdbc:postgresql://localhost:5432/proddb}
username: ${DB_USERNAME:produser}
password: ${DB_PASSWORD:prodpass}
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
jpa:
hibernate:
ddl-auto: validate
show-sql: false
properties:
hibernate:
jdbc:
batch_size: 20
order_inserts: true
order_updates: true
logging:
level:
root: WARN
com.myapp: INFO
file:
name: /var/log/myapp/application.log
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
app:
features:
cache-enabled: true
email-enabled: true
analytics-enabled: true
services:
payment:
url: https://payment.myapp.com
timeout: 10000
notification:
url: https://notification.myapp.com
timeout: 5000
security:
cors:
allowed-origins: "https://myapp.com"
require-ssl: true
management:
endpoints:
web:
exposure:
include: health,metrics,info
endpoint:
health:
show-details: when_authorized
show-components: when_authorized
Profile Activation Methods
1. Programmatic Profile Activation
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.env.Environment;
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(MyApplication.class);
// Method 1: Set active profiles programmatically
app.setAdditionalProfiles("dev", "local");
// Method 2: Use default profiles
app.setDefaultProperties(Collections.singletonMap(
"spring.profiles.default", "dev"
));
app.run(args);
}
}
2. Configuration Class for Profiles
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.context.annotation.PropertySource;
@Configuration
public class ProfileConfig {
@Configuration
@Profile("dev")
@PropertySource("classpath:application-dev.properties")
static class DevConfig {
// Development-specific beans
}
@Configuration
@Profile("test")
@PropertySource({"classpath:application-test.properties",
"classpath:test-overrides.properties"})
static class TestConfig {
// Test-specific beans
}
@Configuration
@Profile("prod")
@PropertySource("classpath:application-prod.properties")
static class ProdConfig {
// Production-specific beans
}
}
Profile-Specific Beans
1. Conditional Bean Configuration
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@Configuration
public class ServiceConfiguration {
// Development: In-memory database
@Bean
@Profile({"dev", "test"})
public DataSource devDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.setName("devdb")
.build();
}
// Production: External database
@Bean
@Profile("prod")
public DataSource prodDataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(env.getProperty("spring.datasource.url"));
dataSource.setUsername(env.getProperty("spring.datasource.username"));
dataSource.setPassword(env.getProperty("spring.datasource.password"));
return dataSource;
}
// Development: Mock email service
@Bean
@Profile({"dev", "test"})
public EmailService mockEmailService() {
return new MockEmailService();
}
// Production: Real email service
@Bean
@Profile("prod")
public EmailService prodEmailService() {
return new SMTPEmailService(
env.getProperty("app.email.host"),
Integer.parseInt(env.getProperty("app.email.port"))
);
}
// Feature flags based on profiles
@Bean
@Profile("!test")
public AnalyticsService analyticsService() {
return new GoogleAnalyticsService();
}
@Bean
@Profile("test")
public AnalyticsService mockAnalyticsService() {
return new MockAnalyticsService();
}
}
2. Conditional Configuration with @Conditional
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
@Configuration
public class ConditionalConfiguration {
@Bean
@ConditionalOnProperty(name = "app.features.cache-enabled", havingValue = "true")
public CacheManager cacheManager() {
return new RedisCacheManager(redisTemplate());
}
@Bean
@ConditionalOnExpression("'${spring.profiles.active}'.contains('dev')")
public DevelopmentTools developmentTools() {
return new DevelopmentTools();
}
@Bean
@ConditionalOnProperty(prefix = "app.security", name = "require-ssl", havingValue = "true")
public SecurityConfig sslSecurityConfig() {
return new SSLSecurityConfig();
}
}
Advanced Profile Strategies
1. Composite Profiles
# application-cloud.properties
spring.cloud.aws.region=us-east-1
app.deployment.env=cloud
app.services.discovery.enabled=true
# application-aws.properties
app.cloud.provider=aws
app.services.s3.enabled=true
# application-azure.properties
app.cloud.provider=azure
app.services.blob-storage.enabled=true
# application-docker.properties
app.deployment.containerized=true
spring.datasource.url=jdbc:postgresql://${DB_HOST:localhost}:5432/${DB_NAME:mydb}
2. Profile Groups (Spring Boot 2.4+)
# In application.properties spring.profiles.group.dev=dev,local,debug spring.profiles.group.test=test,integration spring.profiles.group.prod=prod,cloud,monitoring spring.profiles.group.cloud=cloud,aws,kubernetes
# In application.yml spring: profiles: group: dev: "dev,local,debug" test: "test,integration" prod: "prod,cloud,monitoring" cloud: "cloud,aws,kubernetes"
Property Injection and Usage
1. Configuration Properties Classes
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.Map;
@Component
@ConfigurationProperties(prefix = "app")
@Validated
public class ApplicationProperties {
@NotBlank
private String name;
private String version;
private Features features;
private Map<String, Service> services;
private Security security;
// Static nested classes for structured properties
public static class Features {
private boolean cacheEnabled;
private boolean emailEnabled;
private boolean analyticsEnabled;
// Getters and setters
public boolean isCacheEnabled() { return cacheEnabled; }
public void setCacheEnabled(boolean cacheEnabled) { this.cacheEnabled = cacheEnabled; }
// ... other getters/setters
}
public static class Service {
private String url;
private int timeout = 5000;
private int retryAttempts = 3;
// Getters and setters
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
// ... other getters/setters
}
public static class Security {
private boolean requireSsl = false;
private Cors cors = new Cors();
public static class Cors {
private String[] allowedOrigins = {"*"};
private String[] allowedMethods = {"GET", "POST", "PUT", "DELETE"};
// Getters and setters
public String[] getAllowedOrigins() { return allowedOrigins; }
public void setAllowedOrigins(String[] allowedOrigins) { this.allowedOrigins = allowedOrigins; }
// ... other getters/setters
}
// Getters and setters
public boolean isRequireSsl() { return requireSsl; }
public void setRequireSsl(boolean requireSsl) { this.requireSsl = requireSsl; }
// ... other getters/setters
}
// Getters and setters for top-level properties
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
public Features getFeatures() { return features; }
public void setFeatures(Features features) { this.features = features; }
public Map<String, Service> getServices() { return services; }
public void setServices(Map<String, Service> services) { this.services = services; }
public Security getSecurity() { return security; }
public void setSecurity(Security security) { this.security = security; }
}
2. Using @Value Annotation
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class ServiceClient {
@Value("${app.services.payment.url}")
private String paymentServiceUrl;
@Value("${app.services.payment.timeout:5000}")
private int paymentServiceTimeout;
@Value("${app.features.cache-enabled:false}")
private boolean cacheEnabled;
@Value("${spring.profiles.active:default}")
private String activeProfiles;
public void processPayment() {
if (activeProfiles.contains("dev")) {
System.out.println("Development mode - using: " + paymentServiceUrl);
}
// Implementation
}
}
3. Environment-Aware Services
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
@Service
public class EnvironmentAwareService {
private final Environment environment;
private final ApplicationProperties appProperties;
public EnvironmentAwareService(Environment environment,
ApplicationProperties appProperties) {
this.environment = environment;
this.appProperties = appProperties;
}
public boolean isDevelopment() {
return environment.acceptsProfiles(Profiles.of("dev", "local"));
}
public boolean isProduction() {
return environment.acceptsProfiles(Profiles.of("prod"));
}
public boolean isFeatureEnabled(String feature) {
switch (feature) {
case "cache":
return appProperties.getFeatures().isCacheEnabled();
case "email":
return appProperties.getFeatures().isEmailEnabled();
case "analytics":
return appProperties.getFeatures().isAnalyticsEnabled();
default:
return false;
}
}
public String getServiceUrl(String serviceName) {
ApplicationProperties.Service service =
appProperties.getServices().get(serviceName);
return service != null ? service.getUrl() : null;
}
public void logEnvironmentInfo() {
String[] activeProfiles = environment.getActiveProfiles();
if (activeProfiles.length == 0) {
System.out.println("No active profiles - using default configuration");
} else {
System.out.println("Active profiles: " + String.join(", ", activeProfiles));
}
}
}
Testing with Profiles
1. Test Configuration
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;
// Method 1: Using @ActiveProfiles
@SpringBootTest
@ActiveProfiles({"test", "integration"})
class IntegrationTest {
@Test
void testWithTestProfile() {
// Test implementation
}
}
// Method 2: Using @TestPropertySource
@SpringBootTest
@TestPropertySource(properties = {
"app.features.cache-enabled=false",
"app.services.payment.url=http://localhost:8081/mock-payment",
"spring.datasource.url=jdbc:h2:mem:testdb"
})
class PropertySourceTest {
@Test
void testWithCustomProperties() {
// Test implementation
}
}
// Method 3: Profile-specific test properties
@SpringBootTest
@ActiveProfiles("test")
class ProfileSpecificTest {
@Test
void testWithProfileProperties() {
// Uses application-test.properties
}
}
2. Test Configuration Classes
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
@TestConfiguration
@Profile("test")
public class TestConfig {
@Bean
@Primary
public EmailService testEmailService() {
return new MockEmailService();
}
@Bean
@Primary
public PaymentService testPaymentService() {
return new MockPaymentService();
}
@Bean
public TestDataLoader testDataLoader() {
return new TestDataLoader();
}
}
// Usage in tests
@SpringBootTest
@ActiveProfiles("test")
@Import(TestConfig.class)
class ServiceTest {
@Autowired
private EmailService emailService; // Injects MockEmailService
@Test
void testService() {
// Test with mocked dependencies
}
}
Deployment and External Configuration
1. External Property Files
# Command line to run with external config java -jar myapp.jar \ --spring.config.location=file:/etc/myapp/application.properties \ --spring.profiles.active=prod # Or using environment variables export SPRING_PROFILES_ACTIVE=prod export SPRING_CONFIG_LOCATION=file:/etc/myapp/ java -jar myapp.jar
2. Docker and Kubernetes Configuration
Dockerfile:
FROM openjdk:17-jre-slim # Copy application COPY target/myapp.jar /app/myapp.jar # Create config directory RUN mkdir -p /etc/myapp/ # External configuration volume VOLUME /etc/myapp # Run with external config CMD ["java", "-jar", "/app/myapp.jar", \ "--spring.config.location=file:/etc/myapp/application.properties"]
Kubernetes ConfigMap:
apiVersion: v1 kind: ConfigMap metadata: name: myapp-config data: application-prod.properties: | app.name=MyApplication server.port=8080 spring.datasource.url=jdbc:postgresql://db-host:5432/proddb app.features.cache-enabled=true app.services.payment.url=https://payment-service application-cloud.properties: | app.deployment.env=kubernetes app.services.discovery.enabled=true management.endpoints.web.exposure.include=health,metrics,prometheus
Kubernetes Deployment:
apiVersion: apps/v1 kind: Deployment metadata: name: myapp spec: replicas: 3 selector: matchLabels: app: myapp template: metadata: labels: app: myapp spec: containers: - name: myapp image: myapp:latest ports: - containerPort: 8080 env: - name: SPRING_PROFILES_ACTIVE value: "prod,cloud,kubernetes" - name: DB_USERNAME valueFrom: secretKeyRef: name: db-secret key: username - name: DB_PASSWORD valueFrom: secretKeyRef: name: db-secret key: password volumeMounts: - name: config-volume mountPath: /etc/myapp volumes: - name: config-volume configMap: name: myapp-config
Best Practices
1. Organization and Structure
# Best practices for property organization
# 1. Use meaningful prefixes
app.business.orders.max-per-page=50
app.business.orders.auto-cancel-timeout=24h
app.security.jwt.secret=${JWT_SECRET}
app.security.jwt.expiration=3600
app.integration.payment.timeout=30s
app.integration.payment.retry-attempts=3
# 2. Use profiles for environment-specific values
# 3. Externalize sensitive data (use environment variables)
# 4. Use consistent naming conventions
# 5. Group related properties together
2. Security Considerations
@Component
public class SecurityBestPractices {
// Never commit secrets to version control
// Use environment variables or secret management
@Value("${DB_PASSWORD:}") // Default empty, must be provided
private String dbPassword;
@Value("${API_KEY:}")
private String apiKey;
// Use configuration classes with validation
@ConfigurationProperties(prefix = "app.security")
@Validated
public class SecurityProperties {
@NotBlank
private String secretKey;
// Getters and setters
}
}
3. Monitoring and Validation
@Component
public class ConfigurationValidator {
private final Environment environment;
private final ApplicationProperties appProperties;
public ConfigurationValidator(Environment environment,
ApplicationProperties appProperties) {
this.environment = environment;
this.appProperties = appProperties;
}
@PostConstruct
public void validateConfiguration() {
validateProfiles();
validateRequiredProperties();
logConfiguration();
}
private void validateProfiles() {
String[] activeProfiles = environment.getActiveProfiles();
if (activeProfiles.length == 0) {
logger.warn("No active profiles set. Using default configuration.");
}
for (String profile : activeProfiles) {
if (!isValidProfile(profile)) {
throw new IllegalStateException("Invalid profile: " + profile);
}
}
}
private void validateRequiredProperties() {
if (environment.containsProperty("app.security.jwt.secret") &&
"default-secret-key".equals(environment.getProperty("app.security.jwt.secret"))) {
logger.error("JWT secret is set to default value. This is insecure!");
}
}
private void logConfiguration() {
if (logger.isInfoEnabled()) {
logger.info("Active profiles: {}", String.join(", ", environment.getActiveProfiles()));
logger.info("Application name: {}", appProperties.getName());
logger.info("Cache enabled: {}", appProperties.getFeatures().isCacheEnabled());
}
}
private boolean isValidProfile(String profile) {
return Set.of("dev", "test", "prod", "local", "cloud").contains(profile);
}
}
This comprehensive guide covers profile-specific application properties in Java/Spring Boot applications, including configuration, testing, deployment, and best practices for managing environment-specific settings.