Spring Boot
Environment & Profiles
Spring's Environment abstraction provides unified access to configuration properties from all sources — property files, environment variables, system properties, and more. Profiles extend this by activating different beans and configurations per environment. Together they enable a single application artifact to behave correctly in development, testing, staging, and production without code changes.
The Environment Abstraction
Spring's Environment interface provides a unified abstraction over all configuration sources — property files, system properties, environment variables, JNDI, servlet parameters, and more. Rather than reading from each source directly, you access all of them through a single Environment API with a defined precedence order.
The Environment has two key responsibilities: property resolution (reading configuration values from any source in priority order) and profile management (tracking which profiles are active and which are default).
In Spring Boot, the Environment is automatically populated from multiple sources in a specific precedence order. Higher-priority sources override lower-priority ones — this is what allows environment variables to override application.properties and command-line arguments to override everything else.
Property Sources and Precedence
Spring Boot loads properties from many sources. Understanding the precedence order is essential for predictable configuration — especially when the same property appears in multiple sources.
Java
// Property source precedence (highest to lowest):
// Higher sources override lower sources when the same property key exists
// 1. Command-line arguments:
// java -jar app.jar --server.port=9090
// 2. SPRING_APPLICATION_JSON environment variable:
// SPRING_APPLICATION_JSON='{"server.port":9090}'
// 3. OS environment variables:
// SERVER_PORT=9090
// (Spring converts SERVER_PORT → server.port via relaxed binding)
// 4. Java system properties (-D flags):
// java -Dserver.port=9090 -jar app.jar
// 5. application-{profile}.properties (outside JAR):
// ./config/application-prod.properties
// 6. application.properties (outside JAR):
// ./config/application.properties or ./application.properties
// 7. application-{profile}.properties (inside JAR, classpath):
// src/main/resources/application-prod.properties
// 8. application.properties (inside JAR, classpath):
// src/main/resources/application.properties
// 9. @PropertySource annotations on @Configuration classes
// 10. Default properties set via SpringApplication.setDefaultProperties()
// Accessing properties via Environment:
@Service
public class ConfigurationInspector {
private final Environment environment;
public ConfigurationInspector(Environment environment) {
this.environment = environment;
}
public void inspectProperties() {
// Read a property (null if missing):
String dbUrl = environment.getProperty("spring.datasource.url");
// Read with default value:
String port = environment.getProperty("server.port", "8080");
// Read as specific type:
Integer maxPool = environment.getProperty(
"spring.datasource.hikari.maximum-pool-size",
Integer.class,
10
);
// Require a property — throws MissingRequiredPropertiesException if absent:
String jwtSecret = environment.getRequiredProperty("app.jwt.secret");
// Check if a property exists:
boolean hasMetrics = environment.containsProperty("management.metrics.enabled");
// Check active profiles:
String[] activeProfiles = environment.getActiveProfiles();
String[] defaultProfiles = environment.getDefaultProfiles();
// Check if a specific profile is active:
boolean isProd = environment.acceptsProfiles(Profiles.of("prod"));
boolean isNotProd = environment.acceptsProfiles(Profiles.of("!prod"));
boolean isProdOrStaging = environment.acceptsProfiles(Profiles.of("prod | staging"));
boolean isDevAndDebug = environment.acceptsProfiles(Profiles.of("dev & debug"));
}
}application.properties and application.yml
Spring Boot supports both .properties and .yml formats. Both are functionally equivalent — choose based on team preference. YAML supports hierarchical structure naturally; .properties files are simpler and more universally familiar.
application.properties
# application.properties — flat key-value pairs:
server.port=8080
server.servlet.context-path=/api
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root
spring.datasource.password=${DB_PASSWORD}
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.minimum-idle=5
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=false
spring.jpa.open-in-view=false
logging.level.root=INFO
logging.level.com.example=DEBUG
logging.pattern.console=%d{HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
app.jwt.secret=${JWT_SECRET}
app.jwt.expiration=86400
app.mail.host=smtp.gmail.com
app.mail.port=587Profile-Specific Property Files
Profile-specific files are the primary mechanism for environment-specific configuration. Spring Boot automatically loads the profile-specific file in addition to the base application.properties when a profile is active.
application.properties
# File naming convention:
# application.properties → loaded for ALL profiles (base config)
# application-dev.properties → loaded ADDITIONALLY when 'dev' is active
# application-prod.properties → loaded ADDITIONALLY when 'prod' is active
# application-test.properties → loaded ADDITIONALLY when 'test' is active
# Profile-specific values OVERRIDE base values for the same key
# application.properties (base — shared by all environments):
server.port=8080
spring.jpa.show-sql=false
logging.level.root=INFO
app.name=MyApplication
app.version=2.1.0
# application-dev.properties (development overrides):
spring.datasource.url=jdbc:h2:mem:devdb
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create-drop
logging.level.com.example=DEBUG
logging.level.org.springframework.web=DEBUG
app.mail.host=localhost
app.mail.port=1025
# application-prod.properties (production settings):
spring.datasource.url=${DATABASE_URL}
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}
spring.datasource.hikari.maximum-pool-size=20
spring.jpa.hibernate.ddl-auto=validate
logging.level.root=WARN
logging.level.com.example=INFO
app.mail.host=smtp.sendgrid.net
app.mail.port=465
server.ssl.enabled=true
# application-test.properties (test overrides):
spring.datasource.url=jdbc:h2:mem:testdb;MODE=MySQL
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=create-drop
spring.mail.host=localhost
app.jwt.secret=test-secret-key-for-testing-only-not-for-production
logging.level.root=WARNActivating Profiles
Profiles are activated through several mechanisms. Understanding each activation method and its precedence lets you control the active profile from any layer of your deployment pipeline.
application.properties
# Method 1 — application.properties (lowest priority for profile activation):
spring.profiles.active=dev
# Method 2 — Profile-specific override (useful for profile groups):
# application-production.properties:
# spring.profiles.active=prod,metrics,monitoring
# Method 3 — Environment variable (common in containers):
# SPRING_PROFILES_ACTIVE=prod
# Method 4 — JVM system property:
# java -Dspring.profiles.active=prod -jar app.jar
# Method 5 — Command-line argument (highest priority):
# java -jar app.jar --spring.profiles.active=prod
# Method 6 — Programmatic activation:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(Application.class);
app.setAdditionalProfiles("metrics"); // add a profile programmatically
app.run(args);
}
}
// Multiple profiles — comma-separated:
// spring.profiles.active=prod,metrics,eu-region
// Profile groups (Spring Boot 2.4+) — activate multiple with one name:
// application.properties:
// spring.profiles.group.production=prod,metrics,monitoring,security
// spring.profiles.group.development=dev,local-db,mock-mail
// Activate the group: --spring.profiles.active=production
// All four profiles become active: prod, metrics, monitoring, security
// Default profile — active when NO other profile is set:
// spring.profiles.default=dev
// If spring.profiles.active is set to anything, this is ignored
// Check active profiles at runtime:
@Component
public class ProfileLogger implements ApplicationRunner {
private final Environment environment;
public ProfileLogger(Environment environment) {
this.environment = environment;
}
@Override
public void run(ApplicationArguments args) {
String[] profiles = environment.getActiveProfiles();
if (profiles.length == 0) {
System.out.println("No active profiles — using defaults");
} else {
System.out.println("Active profiles: " + String.join(", ", profiles));
}
}
}@Profile on Beans and Configuration
@Profile conditionally registers beans based on the active profile. Beans annotated with @Profile are only created when the specified profile is active — enabling environment-specific implementations without if-else logic in your code.
Java
// @Profile on @Component — bean only created in specified profile:
@Component
@Profile("dev")
public class MockEmailService implements EmailService {
@Override
public void send(String to, String subject, String body) {
System.out.println("MOCK EMAIL → " + to + ": " + subject);
// No real email sent in development
}
}
@Component
@Profile("prod")
public class SendGridEmailService implements EmailService {
private final SendGrid sendGrid;
public SendGridEmailService(@Value("${sendgrid.api.key}") String apiKey) {
this.sendGrid = new SendGrid(apiKey);
}
@Override
public void send(String to, String subject, String body) {
// Real SendGrid email sending
Request request = new Request();
request.setEndpoint("mail/send");
sendGrid.api(request);
}
}
// @Profile with NOT operator — active in every profile EXCEPT prod:
@Component
@Profile("!prod")
public class DevDataSeeder implements CommandLineRunner {
private final UserRepository userRepository;
public DevDataSeeder(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public void run(String... args) {
// Seed test data in dev and test — never in prod:
if (userRepository.count() == 0) {
userRepository.save(new User("admin@test.com", "Admin", "ADMIN"));
userRepository.save(new User("user@test.com", "Test User", "USER"));
System.out.println("Test data seeded for non-production environment");
}
}
}
// @Profile on @Configuration — entire config class conditional:
@Configuration
@Profile("dev")
public class DevConfiguration {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:schema.sql")
.addScript("classpath:dev-data.sql")
.build();
}
@Bean
public StorageService storageService() {
return new LocalFileStorageService("/tmp/dev-uploads");
}
}
@Configuration
@Profile("prod")
public class ProductionConfiguration {
@Bean
public DataSource dataSource(DataSourceProperties props) {
return DataSourceBuilder.create()
.url(props.getUrl())
.username(props.getUsername())
.password(props.getPassword())
.build();
}
@Bean
public StorageService storageService(S3Properties props) {
return new AwsS3StorageService(props.getBucket(), props.getRegion());
}
}
// Multiple profiles — active if ANY listed profile is active:
@Component
@Profile({"dev", "test"})
public class MockPaymentService implements PaymentService {
@Override
public PaymentResult charge(String customerId, BigDecimal amount) {
return new PaymentResult("mock-txn-" + UUID.randomUUID(), PaymentStatus.SUCCESS);
}
}
// Complex profile expressions (Spring 5.1+):
@Component
@Profile("prod & !legacy") // prod AND NOT legacy
public class ModernProdService { }
@Component
@Profile("dev | test") // dev OR test
public class NonProdHelper { }Environment-Specific Beans with @ConfigurationProperties
@ConfigurationProperties combined with profiles gives you fully typed, validated, environment-specific configuration — the most robust pattern for managing settings across environments.
Java
// Define typed properties class:
@ConfigurationProperties(prefix = "app.payment")
@Validated
public class PaymentProperties {
@NotBlank
private String provider;
@NotBlank
private String apiKey;
private String webhookSecret;
@Min(1) @Max(10)
private int maxRetries = 3;
private boolean sandboxMode = false;
// getters and setters
}
// Register with @EnableConfigurationProperties or @Component:
@Configuration
@EnableConfigurationProperties(PaymentProperties.class)
public class PaymentConfig {
@Bean
public PaymentService paymentService(PaymentProperties props) {
return switch (props.getProvider()) {
case "stripe" -> new StripePaymentService(
props.getApiKey(),
props.getWebhookSecret(),
props.isSandboxMode()
);
case "paypal" -> new PaypalPaymentService(
props.getApiKey(),
props.isSandboxMode()
);
default -> throw new IllegalStateException(
"Unknown payment provider: " + props.getProvider()
);
};
}
}
# application-dev.properties:
# app.payment.provider=stripe
# app.payment.api-key=sk_test_abc123
# app.payment.sandbox-mode=true
# app.payment.max-retries=1
# application-prod.properties:
# app.payment.provider=stripe
# app.payment.api-key=${STRIPE_API_KEY}
# app.payment.webhook-secret=${STRIPE_WEBHOOK_SECRET}
# app.payment.sandbox-mode=false
# app.payment.max-retries=3
// @ConfigurationProperties validation catches misconfiguration at startup:
// Missing app.payment.provider → ConstraintViolationException at startup
// max-retries=15 → ConstraintViolationException (exceeds @Max(10))
// Better than discovering missing config on the first payment request@Profile in Tests
Profile management in tests is critical for loading the right test configuration, activating mock beans, and ensuring test isolation from production settings.
Java
// @ActiveProfiles — activate profiles for a test class:
@SpringBootTest
@ActiveProfiles("test")
class UserServiceIntegrationTest {
// Loads application-test.properties
// Creates all @Profile("test") beans
// Creates all @Profile("!prod") beans
@Autowired UserService userService;
}
// Multiple profiles in tests:
@SpringBootTest
@ActiveProfiles({"test", "security-disabled"})
class ApiIntegrationTest {
@Autowired MockMvc mockMvc;
}
// @TestPropertySource — override specific properties without a profile:
@SpringBootTest
@TestPropertySource(properties = {
"app.jwt.expiration=60",
"app.payment.sandbox-mode=true",
"spring.jpa.show-sql=true"
})
class JwtServiceTest { }
// @TestPropertySource with a file:
@SpringBootTest
@TestPropertySource(locations = "classpath:test-overrides.properties")
class SpecialConfigTest { }
// Profile-specific test beans — only active during tests:
@TestConfiguration
public class TestBeanOverrides {
@Bean
@Primary
@Profile("test")
public PaymentService mockPaymentService() {
return mock(PaymentService.class);
}
@Bean
@Primary
@Profile("test")
public EmailService captureEmailService() {
return new EmailCaptor(); // captures emails for assertion
}
}
// Use the test configuration:
@SpringBootTest
@ActiveProfiles("test")
@Import(TestBeanOverrides.class)
class FullIntegrationTest {
@Autowired PaymentService paymentService; // gets mock from TestBeanOverrides
@Test
void testCheckout() {
when(paymentService.charge(any(), any()))
.thenReturn(new PaymentResult("test-txn", PaymentStatus.SUCCESS));
// test checkout flow...
}
}
// Conditional test execution based on profile:
@SpringBootTest
@ActiveProfiles("integration")
@EnabledIfSystemProperty(named = "run.integration.tests", matches = "true")
class ExternalServiceIntegrationTest {
// Only runs when -Drun.integration.tests=true is set
// Used to separate fast unit tests from slow integration tests in CI
}Accessing Environment Programmatically
Beyond @Value and @ConfigurationProperties, you sometimes need programmatic access to the Environment — for dynamic configuration lookups, conditional logic based on properties, or bridging Spring's environment into non-Spring code.
Java
// Inject Environment directly:
@Service
public class FeatureFlagService {
private final Environment environment;
public FeatureFlagService(Environment environment) {
this.environment = environment;
}
public boolean isEnabled(String featureName) {
return environment.getProperty(
"app.features." + featureName + ".enabled",
Boolean.class,
false
);
}
public <T> T getConfig(String key, Class<T> type, T defaultValue) {
return environment.getProperty(key, type, defaultValue);
}
}
// EnvironmentPostProcessor — modify the environment before the context is created:
// Useful for adding custom property sources programmatically
public class VaultPropertySourcePostProcessor implements EnvironmentPostProcessor {
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application) {
// Load secrets from HashiCorp Vault and add to environment:
Map<String, Object> vaultSecrets = loadFromVault();
MapPropertySource vaultSource = new MapPropertySource("vault", vaultSecrets);
// Add at highest priority — overrides everything else:
environment.getPropertySources().addFirst(vaultSource);
}
private Map<String, Object> loadFromVault() {
// Connect to Vault and retrieve secrets
return Map.of(
"spring.datasource.password", "vault-retrieved-password",
"app.jwt.secret", "vault-retrieved-jwt-secret"
);
}
}
// Register in META-INF/spring.factories:
// org.springframework.boot.env.EnvironmentPostProcessor=// com.example.VaultPropertySourcePostProcessor
// EnvironmentAware — implement to receive the environment:
@Component
public class DynamicConfigService implements EnvironmentAware {
private Environment environment;
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
public String getConnectionString(String serviceName) {
String host = environment.getProperty("services." + serviceName + ".host", "localhost");
int port = environment.getProperty("services." + serviceName + ".port", Integer.class, 8080);
return "http://" + host + ":" + port;
}
}