Profile-Specific Application Properties in Java

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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper