Spring Boot
Environment & Profiles
How Spring Boot 3 configuration works: the Environment abstraction, the exact order in which property sources override each other, relaxed binding for environment variables, profile-specific files, multi-document YAML with on-profile, profile groups, @Profile beans, typed and validated @ConfigurationProperties, spring.config.import for secrets, and profile handling in tests. One artifact, correct behaviour in dev, staging and production.
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 populated from many sources in a specific order. Higher-priority sources override lower-priority ones. This is what allows environment variables to override application.properties and command-line arguments to override both.
Property Sources and Precedence
When the same key appears in several places, the highest source in this list wins (Spring Boot 3.x):
1. Devtools global settings (when Spring Boot Devtools is active)
2. @TestPropertySource annotations on tests
3. The properties attribute on @SpringBootTest and test slice annotations
4. Command-line arguments (--server.port=9090)
5. SPRING_APPLICATION_JSON (inline JSON in an environment variable or system property)
6. ServletConfig init parameters
7. ServletContext init parameters
8. JNDI attributes
9. Java system properties (-Dserver.port=9090)
10. OS environment variables (SERVER_PORT=9090)
11. Profile-specific application properties outside the jar (./config/application-prod.properties)
12. Profile-specific application properties inside the jar (classpath application-prod.properties)
13. Application properties outside the jar (./config/application.properties, ./application.properties)
14. Application properties inside the jar (classpath application.properties)
15. @PropertySource annotations on @Configuration classes
16. Default properties set with SpringApplication.setDefaultProperties
Two orderings are easy to get backwards. Java system properties (-D) override OS environment variables, not the other way round. And profile-specific files, whether inside or outside the jar, override the plain application.properties files. Within the file locations, a later location wins: ./config/ overrides ./, and the filesystem overrides the classpath.
@PropertySource is the lowest-priority file source and is processed late, so it cannot configure things Spring Boot reads early (logging, server settings). It also cannot load YAML without a custom factory. Prefer application.properties or spring.config.import.
Reading Properties from the Environment
Inject Environment when you need dynamic lookups. For fixed settings, prefer @Value or @ConfigurationProperties. The API below shows the common calls, including the profile checks and their expression syntax.
Java
@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 with default:
Integer maxPool = environment.getProperty(
"spring.datasource.hikari.maximum-pool-size", Integer.class, 10);
// Require a property: throws IllegalStateException if it is absent
String jwtSecret = environment.getRequiredProperty("app.jwt.secret");
// Check if a property exists:
boolean hasPoolSize = environment.containsProperty(
"spring.datasource.hikari.maximum-pool-size");
// Active profiles, and the default profiles (["default"] unless you changed it):
String[] activeProfiles = environment.getActiveProfiles();
String[] defaultProfiles = environment.getDefaultProfiles();
// Profile expressions:
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"));
}
}Environment Variables and Relaxed Binding
Environment variables cannot contain dots or dashes, so Spring Boot applies relaxed binding: a property key is converted by replacing dots with underscores, removing dashes, and using upper case. SERVER_PORT therefore sets server.port, and SPRING_DATASOURCE_HIKARI_MAXIMUMPOOLSIZE sets spring.datasource.hikari.maximum-pool-size. List items use an index, so APP_ORIGINS_0 and APP_ORIGINS_1 populate app.origins[0] and app.origins[1].
Because environment variables rank above every properties file, they are the standard way to configure containers: the same image runs everywhere and only the variables change. Keep secrets in variables, never in files committed to version control. Inside a properties file you can reference variables with a placeholder and an optional default, for example ${DB_PASSWORD:changeme}. Use a default only for non-sensitive values.
Shell
# Typical container configuration
SPRING_PROFILES_ACTIVE=prod
SERVER_PORT=9090
SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/app
SPRING_DATASOURCE_PASSWORD=change-me-via-secret-store
SPRING_DATASOURCE_HIKARI_MAXIMUMPOOLSIZE=20
APP_ORIGINS_0=https://app.example.com
APP_ORIGINS_1=https://admin.example.comapplication.properties and application.yml
Spring Boot supports both .properties and .yml formats, and both bind the same way. YAML expresses hierarchy naturally and supports several documents in one file (see the multi-document section below). Properties files are flatter and more familiar. If both formats exist in the same location, the .properties file wins, so do not keep the same keys in both.
Reference environment variables with ${NAME}, and give non-sensitive values a default with ${NAME:default}.
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
spring.mail.host=smtp.gmail.com
spring.mail.port=587Profile-Specific Property Files
Profile-specific files are the primary mechanism for environment-specific configuration. When a profile is active, Spring Boot loads application-{profile}.properties in addition to the base file, and the profile-specific values override the base values for the same key. When several profiles are active, later ones in the list override earlier ones.
Keep the base file for settings shared by every environment, and put only the differences in the profile files.
application.properties
# File naming convention:
# application.properties → always loaded (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
# 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
spring.mail.host=localhost
spring.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
spring.mail.host=smtp.sendgrid.net
spring.mail.port=587
server.shutdown=graceful
# 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=WARNMulti-Document Files with on-profile
Instead of one file per profile, a single YAML file can hold several documents separated by ---, each activated by a profile with spring.config.activate.on-profile. The same works in a properties file using a line containing only #--- as the separator. Documents without an on-profile condition apply always, and later documents override earlier ones. The old spring.profiles key used for this before Spring Boot 2.4 is no longer supported.
yaml
# application.yml
app:
name: MyApplication
server:
port: 8080
---
spring:
config:
activate:
on-profile: dev
datasource:
url: jdbc:h2:mem:devdb
server:
port: 8081
---
spring:
config:
activate:
on-profile: prod
datasource:
url: ${DATABASE_URL}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
logging:
level:
root: WARNActivating Profiles
Profiles can be activated at several layers. The strongest source wins, so a command-line argument overrides an environment variable, which overrides a value in application.properties. In production, set the profile from the outside (an environment variable or a command-line argument) and avoid committing a hardcoded spring.profiles.active=prod that could silently apply to a developer's machine.
Two rules changed in Spring Boot 2.4. First, spring.profiles.active cannot be set inside a profile-specific file or on-profile document: Spring Boot refuses to start. Use profile groups for that, as shown below. Second, a profile group name activates its members, so activating production below also activates prod, metrics, monitoring and security, together with the group name itself.
application.properties
# Method 1 — application.properties (lowest priority):
spring.profiles.active=dev
# Method 2 — environment variable (common in containers):
# SPRING_PROFILES_ACTIVE=prod
# Method 3 — JVM system property:
# java -Dspring.profiles.active=prod -jar app.jar
# Method 4 — command-line argument (highest priority of these):
# java -jar app.jar --spring.profiles.active=prod
# Several profiles — comma-separated (later profiles override earlier ones):
# spring.profiles.active=prod,metrics,eu-region
# Profile groups (Spring Boot 2.4+) — one name activates several profiles:
spring.profiles.group.production=prod,metrics,monitoring,security
spring.profiles.group.development=dev,local-db,mock-mail
# Activate with: --spring.profiles.active=production
# spring.profiles.include — adds profiles unconditionally to whatever else is active
# (not allowed inside profile-specific files since Spring Boot 2.4):
# spring.profiles.include=common-security
# Default profile — used only when NO profile is active.
# If you set nothing, Spring uses a profile literally named "default"
# and loads application-default.properties:
spring.profiles.default=devActivating and Checking Profiles in Code
You can add profiles programmatically before the context starts, and read the active profiles at runtime, for example to log them at startup. Use a logger instead of System.out so the output follows your logging configuration.
Java
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(Application.class);
app.setAdditionalProfiles("metrics"); // added to whatever else is active
app.run(args);
}
}
@Component
public class ProfileLogger implements ApplicationRunner {
private static final Logger log = LoggerFactory.getLogger(ProfileLogger.class);
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) {
log.info("No active profiles, using default profile(s): {}",
String.join(", ", environment.getDefaultProfiles()));
} else {
log.info("Active profiles: {}", String.join(", ", profiles));
}
}
}@Profile on Beans and Configuration
@Profile registers a bean only when the specified profile is active, which lets you swap implementations without if-else logic in your code. The email example below needs spring-boot-starter-mail, which provides JavaMailSender and configures it from the spring.mail.* properties.
Be careful with negation. @Profile("!prod") is active in staging, in test, and when no profile is set at all. That is a dangerous default for something like a data seeder. Prefer an explicit allow-list such as @Profile("dev"). Also avoid defining a DataSource bean by hand inside a profile configuration: doing so switches off Spring Boot's DataSource auto-configuration, and your spring.datasource.hikari.* settings are then silently ignored. Keep the DataSource in properties and use profile configurations for beans that really differ, such as storage services.
Java
// @Profile on @Component — bean only created in the specified profile:
@Component
@Profile("dev")
public class MockEmailService implements EmailService {
private static final Logger log = LoggerFactory.getLogger(MockEmailService.class);
@Override
public void send(String to, String subject, String body) {
log.info("MOCK EMAIL to {}: {}", to, subject); // nothing is sent in development
}
}
@Component
@Profile("prod")
public class SmtpEmailService implements EmailService {
private final JavaMailSender mailSender; // configured by spring.mail.* properties
public SmtpEmailService(JavaMailSender mailSender) {
this.mailSender = mailSender;
}
@Override
public void send(String to, String subject, String body) {
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(to);
message.setSubject(subject);
message.setText(body);
mailSender.send(message);
}
}
// Seed data only where you mean to. "!prod" would also run in staging
// and when no profile is active.
@Component
@Profile("dev")
public class DevDataSeeder implements CommandLineRunner {
private final UserRepository userRepository;
public DevDataSeeder(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public void run(String... args) {
if (userRepository.count() == 0) {
userRepository.save(new User("admin@test.com", "Admin", "ADMIN"));
userRepository.save(new User("user@test.com", "Test User", "USER"));
}
}
}
// @Profile on @Configuration — the whole class is conditional.
// Only beans that genuinely differ per environment belong here.
@Configuration
@Profile("dev")
public class DevConfiguration {
@Bean
public StorageService storageService() {
return new LocalFileStorageService("/tmp/dev-uploads");
}
}
@Configuration
@Profile("prod")
public class ProductionConfiguration {
@Bean
public StorageService storageService(S3Properties props) {
return new AwsS3StorageService(props.getBucket(), props.getRegion());
}
}
// Several profiles in one annotation — 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);
}
}
// Profile expressions (Spring 5.1+). Mixing & and | needs parentheses:
@Component
@Profile("prod & !legacy") // prod AND NOT legacy
public class ModernProdService { }
@Component
@Profile("dev | test") // dev OR test
public class NonProdHelper { }
@Component
@Profile("(dev | test) & !offline")
public class OnlineNonProdHelper { }Typed Configuration with @ConfigurationProperties
@ConfigurationProperties binds a group of properties to a typed class, and combined with profile files it is the most robust way to manage per-environment settings. Add spring-boot-starter-validation to enforce the Bean Validation annotations; without it, @NotBlank and @Min are silently ignored. When validation fails, the application refuses to start with a ConfigurationPropertiesBindException caused by a BindValidationException that lists every violation, which is far better than finding a missing key on the first payment request.
Never commit real secrets to profile files. A test-mode key like the one below is acceptable in the dev profile; production values come from environment variables.
Java
// 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 it:
@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
//
// Startup fails fast on misconfiguration:
// missing app.payment.provider → bind failure (@NotBlank)
// app.payment.max-retries=15 → bind failure (exceeds @Max(10))Importing Extra Configuration with spring.config.import
spring.config.import (Spring Boot 2.4+) pulls in additional configuration from other files or directories. The optional: prefix means startup continues if the location does not exist; without it, a missing location stops the application. The configtree: prefix is useful for container secrets: each file in the directory becomes a property, with the file name as the key and the file content as the value, which matches how Docker and Kubernetes mount secrets. Imported properties rank above the file that imports them.
application.properties
# application.properties
# Local developer overrides that are never committed:
spring.config.import=optional:file:./local.properties
# Container secrets: /run/secrets/db.password becomes the property "db.password"
spring.config.import=optional:file:./local.properties,optional:configtree:/run/secrets/
spring.datasource.password=${db.password}Profiles in Tests
Profile management in tests loads the right configuration and keeps tests isolated from production settings. @ActiveProfiles replaces the profiles from spring.profiles.active, so the test uses exactly the profiles you list. @TestPropertySource and the properties attribute on @SpringBootTest rank above almost everything else, so they are the right place for one-off overrides.
For replacing a bean with a mock, @MockitoBean (Spring Boot 3.4 and later; @MockBean before that) is simpler than writing a profile-specific test configuration.
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 too
@Autowired UserService userService;
}
// Multiple profiles:
@SpringBootTest
@ActiveProfiles({"test", "security-disabled"})
class ApiIntegrationTest {
@Autowired MockMvc mockMvc;
}
// 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 { }
// Same idea with a file:
@SpringBootTest
@TestPropertySource(locations = "classpath:test-overrides.properties")
class SpecialConfigTest { }
// Replace a bean with a mock:
@SpringBootTest
@ActiveProfiles("test")
class CheckoutTest {
@MockitoBean PaymentService paymentService;
@Test
void checkoutSucceeds() {
when(paymentService.charge(any(), any()))
.thenReturn(new PaymentResult("test-txn", PaymentStatus.SUCCESS));
// test checkout flow...
}
}
// A test-only helper bean, imported explicitly (no profile needed):
@TestConfiguration
public class TestBeanOverrides {
@Bean
@Primary
public EmailService captureEmailService() {
return new EmailCaptor(); // captures emails for assertions
}
}
@SpringBootTest
@ActiveProfiles("test")
@Import(TestBeanOverrides.class)
class EmailFlowTest { }
// Run slow tests only on demand (JUnit 5):
@SpringBootTest
@ActiveProfiles("integration")
@EnabledIfSystemProperty(named = "run.integration.tests", matches = "true")
class ExternalServiceIntegrationTest {
// Runs only with -Drun.integration.tests=true
}Accessing Environment Programmatically
Beyond @Value and @ConfigurationProperties, you sometimes need programmatic access to the Environment for dynamic lookups, feature flags, or bridging Spring's configuration into non-Spring code.
An EnvironmentPostProcessor lets you modify the Environment before the application context is created, for example to add a custom property source. It runs very early, so logging and most beans are not available. Registering a source with addFirst gives it the highest priority, above even command-line arguments, so use addLast or addAfter unless you really want to override everything. For real secret stores, prefer a supported integration such as spring.config.import with Spring Cloud Vault or a configtree mount. Register the post-processor in META-INF/spring.factories on a single line.
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:
public class CustomPropertySourcePostProcessor implements EnvironmentPostProcessor {
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application) {
Map<String, Object> extra = loadExtraProperties();
MapPropertySource source = new MapPropertySource("extra", extra);
// addLast keeps command-line and environment variables in charge.
// addFirst would override everything, including command-line arguments.
environment.getPropertySources().addLast(source);
}
private Map<String, Object> loadExtraProperties() {
return Map.of("app.region", "eu-west-1");
}
}
// META-INF/spring.factories (one line):
// org.springframework.boot.env.EnvironmentPostProcessor=com.example.CustomPropertySourcePostProcessor
// 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;
}
}Common Mistakes
1. Getting the precedence wrong. System properties (-D) beat environment variables, and profile-specific files beat the plain application.properties, inside or outside the jar.
2. Setting spring.profiles.active inside a profile-specific file or on-profile document. Since Spring Boot 2.4 this stops the application from starting. Use profile groups.
3. Using the removed spring.profiles key to label YAML documents. Use spring.config.activate.on-profile.
4. @Profile("!prod") on seeders and mocks. It also runs in staging and when no profile is set. Use an allow-list.
5. Defining a DataSource bean by hand in a profile class. It disables auto-configuration, so spring.datasource.hikari.* is ignored.
6. Forgetting spring-boot-starter-validation. @Validated configuration properties then accept bad values silently.
7. Committing secrets to application-prod.properties. Use environment variables, a secret store, or configtree.
8. Expecting @Value or @ConfigurationProperties to see a profile that is not active. Check the startup log line that lists the active profiles.
9. Hardcoding spring.profiles.active=prod in the committed application.properties, which then applies to every environment.
10. Using @PropertySource for YAML or for settings Spring Boot reads early.
FAQ
How do I set the active profile in Docker or Kubernetes?
Set the SPRING_PROFILES_ACTIVE environment variable on the container. It overrides the value in application.properties without rebuilding the image.
What is the difference between spring.profiles.active and spring.profiles.include?
active selects the profiles, and a stronger source such as an environment variable replaces it. include adds profiles on top of whatever is active, regardless of where active was set.
Why is my application-dev.properties not loaded?
The dev profile is probably not active. Check the file name (application-dev.properties), its location (classpath root, ./config/ or ./), and the startup log, which prints the active profiles.
Which wins: an environment variable or application.properties?
The environment variable. Command-line arguments and system properties also beat application.properties.
Can I use one file for all profiles?
Yes. Use a multi-document YAML file with spring.config.activate.on-profile, or a properties file with #--- separators.
How do I run tests with a different configuration?
Use @ActiveProfiles("test") with an application-test.properties file, and @TestPropertySource for one-off overrides.
Should I use @Value or @ConfigurationProperties?
Use @Value for a single value. Use @ConfigurationProperties for a group of related settings, because it is typed, validated and easier to test.