Application Properties — Practical Demo
Hands-on examples for Application Properties. Each example covers a realistic configuration pattern you will encounter in production Spring Boot projects.
Read the Application Properties note first — particularly the property source priority order and the @ConfigurationProperties vs @Value comparison.
Example 1: Profiles — Switching Config Between Environments
This example shows the multi-environment profile pattern: one base file, two profile overrides.
Project layout:
src/main/resources/
application.yml ← base (always loaded)
application-dev.yml ← dev overrides
application-prod.yml ← prod overrides
spring:
application:
name: order-service
server:
port: 8080
app:
datasource:
url: jdbc:h2:mem:testdb # ← default in-memory DB for local experiments
pool-size: 5
logging:
level:
com.example: DEBUG # ← verbose logging only in dev
spring:
datasource:
show-sql: true
server:
port: 8443
spring:
datasource:
url: ${DB_URL} # ← injected from OS environment variable
username: ${DB_USER}
password: ${DB_PASS}
jpa:
show-sql: false # ← no SQL in prod logs
Activate prod profile:
# Run with prod profile active
java -jar order-service.jar --spring.profiles.active=prod
Expected behavior: application-prod.yml is loaded on top of application.yml. The server.port becomes 8443; datasource values come from env vars; app.datasource.pool-size (set only in base) stays 5.
Profile files are additive overlays. Base values you don't override stay in effect. Only set values that differ per environment.
Example 2: @ConfigurationProperties with Nested Objects and Lists
A realistic payments configuration with nested objects and a list of allowed currencies:
payment:
api-key: sk_test_abc123
timeout-ms: 3000
max-retries: 2
base-url: https://api.stripe.com
allowed-currencies:
- USD
- EUR
- GBP
retry:
initial-delay-ms: 500
multiplier: 2.0
@ConfigurationProperties(prefix = "payment") // ← binds everything under payment.*
@Component
@Validated // ← enables JSR-303 validation at startup
public class PaymentProperties {
@NotBlank // ← startup fails if blank or missing
private String apiKey;
@Min(100) @Max(30000)
private int timeoutMs = 3000; // ← field default as fallback
private int maxRetries = 3;
private String baseUrl;
private List<String> allowedCurrencies = new ArrayList<>();
private RetryConfig retry = new RetryConfig(); // ← nested object
// --- nested class ---
public static class RetryConfig {
private int initialDelayMs = 500;
private double multiplier = 2.0;
// getters and setters
}
// getters and setters for all fields
}
@Service
public class PaymentService {
private final PaymentProperties config; // ← injected like a normal bean
public PaymentService(PaymentProperties config) {
this.config = config;
}
public boolean isCurrencyAllowed(String currency) {
return config.getAllowedCurrencies().contains(currency); // ← no hardcoded strings
}
}
Expected Output — if payment.api-key is blank in any environment, startup fails with:
***************************
APPLICATION FAILED TO START
***************************
Description:
Binding to target org.springframework.validation.BeanPropertyBindingResult...
Field error in object 'payment' on field 'apiKey': rejected value []; must not be blank
@Validated on @ConfigurationProperties gives you self-documenting, fail-fast validation. The error message names the exact property and constraint — no more silent null values causing NPEs at runtime.
Example 3: @Value with Defaults and SpEL
For two one-off values in a utility bean:
@Component
public class FeatureFlags {
@Value("${feature.new-checkout:false}") // ← default false if property is absent
private boolean newCheckoutEnabled;
@Value("${feature.max-items:100}")
private int maxItemsPerOrder;
@Value("#{${feature.max-items:100} * 2}") // ← SpEL: double the max for admins
private int adminMaxItemsPerOrder;
public boolean isNewCheckoutEnabled() {
return newCheckoutEnabled;
}
}
feature.new-checkout=true
feature.max-items=50
Expected behavior in prod: newCheckoutEnabled = true, maxItemsPerOrder = 50, adminMaxItemsPerOrder = 100 (SpEL evaluated at startup).
@Value("${missing.key}") with no default (:{value}) throws IllegalArgumentException at context startup if the property is absent. Always add a default for optional properties.
Example 4: Overriding Config in Tests
Use @TestPropertySource to override specific values for one test class without affecting the main config:
@SpringBootTest
@TestPropertySource(properties = {
"payment.timeout-ms=100", // ← fast timeout to avoid slow tests
"payment.max-retries=1"
})
class PaymentServiceTest {
@Autowired
private PaymentProperties config;
@Test
void timeoutShouldBeOverriddenForTest() {
assertThat(config.getTimeoutMs()).isEqualTo(100); // ← confirm override applied
}
}
Alternative — inline YAML block in the test annotation:
@SpringBootTest(properties = "spring.datasource.url=jdbc:h2:mem:testdb")
class AnonymousConfigTest { ... }
Never modify application.properties for tests. Use @TestPropertySource or the properties = attribute to keep test config isolated from production config.
Exercises
Try these on your own to solidify understanding:
- Easy: Add a custom property
app.greeting=Hellotoapplication.propertiesand inject it with@Value. Add a dev profile file that overrides it toHello (DEV). Verify that activating thedevprofile changes the injected value. - Medium: Create a
@ConfigurationPropertiesclass for anotification.*namespace with fieldsprovider(String),fromAddress(String,@Emailconstraint), andretryCount(int,@Min(0)). Test that startup fails whenfromAddressis missing. - Hard: Write a
@SpringBootTestintegration test that uses@DynamicPropertySourceto overridespring.datasource.urlat runtime (simulating what Testcontainers does). Verify the injectedDataSourceURL matches what you set.
Back to Topic
Return to the Application Properties note for theory, interview questions, and further reading.