Spring Boot Auto-Configuration & Externalized Config
Implements auto-configuration mechanics, externalized configuration binding with @ConfigurationProperties, profile-based activation, custom starter development with conditional annotations, and Actuator health/metrics customization for production-grade Spring Boot applications. When loaded, this skill makes the model write type-safe configuration classes, conditionally registered beans, and production-ready Actuator endpoints using modern Spring Boot 3.x APIs.
TL;DR Checklist
- Verify all
@ConfigurationPropertiesare bound to records or final classes with@ConstructorBinding - Confirm custom starters include
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports - Ensure
@ConditionalOnClass,@ConditionalOnMissingBean, and@ConditionalOnPropertyguard all auto-config beans - Check that Actuator health indicators implement
HealthIndicatorinterface with meaningful UP/DOWN status - Validate profile activation uses
spring.profiles.activeorspring.profiles.includein application.yml - Confirm externalized config uses
.ymlor.yamlformat (not.properties) for nested structures
When to Use
Use this skill when:
- Implementing auto-configuration that should activate only when specific classes or beans are present on the classpath
- Binding external configuration from
application.yml, environment variables, or command-line arguments to typed configuration objects - Creating a custom Spring Boot starter library for internal platforms or public distribution
- Extending Actuator health checks with application-specific component status reporting
- Managing multi-environment configuration (dev, staging, production) with Spring Profiles
When NOT to Use
Avoid this skill for:
- Simple bean registration without conditional logic — use a plain
@Configurationclass instead - Direct database or security configuration — use
spring-data-jpaorspring-security-coreskills - Runtime configuration changes without restart — Spring Boot config is loaded at startup; use Spring Cloud Config for dynamic refresh
- Configuration that requires complex validation beyond type-safe binding — add JSR-380 constraints to
@ConfigurationPropertiesclasses
Core Workflow
Define the External Configuration Model — Create a record or class annotated with
@ConfigurationProperties(prefix = "your.prefix"). Use Java records for immutable configuration, or final classes with@ConstructorBindingconstructors for complex hierarchies. Register the bean in a@Configurationclass using@EnableConfigurationProperties(YourConfig.class)or rely on Spring Boot's component scan with the annotation present.Checkpoint: Every property binding key maps to a real field. Nested objects use separate nested records/classes, not
Map<String, Object>wrappers. Run a compilation check — no missing imports fororg.springframework.boot.context.properties.ConfigurationProperties.Guard Beans with Conditional Annotations — Wrap auto-configuration beans using
@ConditionalOnClass,@ConditionalOnMissingBean,@ConditionalOnProperty, and@ConditionalOnWebApplicationto ensure the configuration activates only when its dependencies exist and no conflicting bean is already registered. Always combine at least two conditions: one for classpath presence (@ConditionalOnClass) and one for bean absence (@ConditionalOnMissingBean).Checkpoint: If removing a dependency JAR causes the auto-config to silently disable without an error, the condition is correct. Verify that
@ConditionalOnProperty(name = "your.feature.enabled", havingValue = "true")gate can be toggled via environment variable override (YOUR_FEATURE_ENABLED=true).Build Custom Starter with AutoConfiguration.imports — Create a starter JAR by placing the fully qualified auto-configuration class names (one per line) into
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. This replaces the deprecatedspring.factoriesapproach used in Spring Boot 2.x. The starter POM should havescope=compilefor runtime dependencies andscope=providedfor API dependencies like Jackson or Hibernate.Checkpoint: After building the starter JAR, inspect it with
jar tf your-starter.jar | grep AutoConfiguration.importsto confirm the file exists. Verify no entries appear inMETA-INF/spring.factories. Run a test app that depends on the starter and confirm the auto-config triggers without explicit@Import.Activate Profiles Strategically — Configure environment-specific overrides using profile-activated
@Configurationclasses annotated with@Profile("profile-name"). Inapplication.yml, usespring.profiles.active: dev,localfor active profiles andspring.profiles.include: commonfor always-loaded defaults. Never hardcode profile names in application logic — resolve them viaEnvironment#getActiveProfiles()or constructor injection of@Value("${spring.profiles.active}").Checkpoint: When launching with
-Dspring.profiles.active=prod, all other profiles must be overridden. Verify the correct DataSource, security settings, and Actuator exposure are active by checking bean registration order in debug logging (--debugflag).Implement Custom Actuator Health Indicator — Create a class implementing
org.springframework.boot.actuate.health.HealthIndicator. Implement thehealth()method to check downstream dependencies (database, cache, message broker) and return eitherHealth.up().withDetail("key", "value").build()orHealth.down().withException(e).build(). Register the bean in an auto-config class with@ConditionalOnProperty(name = "management.health.custom.enabled", havingValue = "true", matchIfMissing = true).Checkpoint: The
/actuator/healthendpoint must return a JSON object with your component's name as a key. Ensure health checks are non-blocking and include timeout logic (useCompletableFuture.supplyAsync()with.orTimeout(3, TimeUnit.SECONDS)). Verify that anOutOfMemoryErroror unhandled exception in the health check does not crash the application — wrap all health checks in try/catch returningHealth.unknown().
Implementation Patterns / Reference Guide
Pattern 1: Type-Safe Configuration Properties Bound to a Record
Modern Spring Boot 3.x works seamlessly with Java records for immutable, type-safe configuration. The record's compact constructor serves as the binding target — no setters needed.
package com.example.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.time.Duration;
import java.util.List;
/**
* Binds properties prefixed with "app.gateway" from application.yml,
* environment variables (APP_GATEWAY_*), and command-line arguments.
*/
@ConfigurationProperties(prefix = "app.gateway")
public record GatewayProperties(
String baseUrl,
Duration connectTimeout,
Duration readTimeout,
boolean retryEnabled,
int maxRetries,
List<String> allowedOrigins
) {
/**
* Compact constructor for validation of bound properties.
*/
public GatewayProperties {
if (baseUrl == null || baseUrl.isBlank()) {
throw new IllegalArgumentException("app.gateway.base-url must be set");
}
if (connectTimeout == null) {
connectTimeout = Duration.ofSeconds(5);
}
if (readTimeout == null) {
readTimeout = Duration.ofMinutes(1);
}
if (maxRetries < 0) {
throw new IllegalArgumentException("app.gateway.max-retries must be >= 0");
}
}
}
# application.yml — corresponding configuration
app:
gateway:
base-url: "https://api.example.com"
connect-timeout: 5s
read-timeout: 60s
retry-enabled: true
max-retries: 3
allowed-origins:
- "https://app.example.com"
- "https://admin.example.com"
Production pitfall:
@ConfigurationPropertieson records works only with Spring Boot 3.2+. For earlier versions, use a final class with@ConstructorBinding. Always provide sensible defaults — unconfigured properties cause startup failures.
Pattern 2: Conditional Auto-Configuration for Custom Starter
This pattern creates a starter that registers beans conditionally based on classpath presence, bean existence, and property flags. The AutoConfiguration.imports file is the registration mechanism for Spring Boot 3.x.
package com.example.autoconfig;
import jakarta.annotation.PostConstruct;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
/**
* Auto-configuration for a custom HTTP client library.
* Activates only when the target library is on the classpath,
* the feature flag is enabled, and no custom HttpClient bean exists.
*/
@AutoConfiguration
@ConditionalOnClass(name = "com.example.httpclient.HttpClient")
@ConditionalOnProperty(prefix = "app.custom-client", name = "enabled", havingValue = "true", matchIfMissing = true)
public class CustomClientAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public com.example.httpclient.HttpClient customHttpClient(
GatewayProperties gatewayProps) {
var client = new com.example.httpclient.HttpClient(gatewayProps.baseUrl());
client.setConnectTimeout(gatewayProps.connectTimeout());
client.setReadTimeout(gatewayProps.readTimeout());
if (gatewayProps.retryEnabled()) {
client.setMaxRetries(gatewayProps.maxRetries());
}
return client;
}
@PostConstruct
void logConfiguration() {
// Logged after all beans are initialized — safe to access properties here
System.out.println("[CustomClient] HttpClient auto-configured for: "
+ gatewayProps.baseUrl());
}
}
# META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.autoconfig.CustomClientAutoConfiguration
Production pitfall: Use
@ConditionalOnClass(name = "...")with the fully qualified class name as a string when you want to avoid importing the dependency at compile time. This is essential for optional dependencies in starters — the starter compiles without the library but activates it when present.
Pattern 3: Custom Actuator Health Indicator with Timeout Protection
Production systems must have robust health endpoints that do not crash the application when downstream services are unavailable.
package com.example.health;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.Map;
/**
* Health indicator for a downstream REST API dependency.
* Implements non-blocking health checks with configurable timeouts
* to prevent the /actuator/health endpoint from hanging or crashing.
*/
@Component
public class DownstreamApiHealthIndicator implements HealthIndicator {
private final String apiBaseUrl;
private final Duration timeout;
public DownstreamApiHealthIndicator(
com.example.config.GatewayProperties gatewayProps) {
this.apiBaseUrl = gatewayProps.baseUrl();
this.timeout = gatewayProps.readTimeout().dividedBy(2); // half of read timeout
}
@Override
public Health health() {
try {
CompletableFuture<Boolean> checkFuture = CompletableFuture.supplyAsync(() -> {
// Simulated health check — in production, use RestTemplate or WebClient
boolean isReachable = checkApiReachability(apiBaseUrl);
return isReachable;
});
Boolean result = checkFuture.orTimeout(
Math.max(timeout.toMillis(), 1000),
TimeUnit.MILLISECONDS
).join();
if (result) {
return Health.up()
.withDetail("apiUrl", apiBaseUrl)
.withDetail("responseTimeMs", System.currentTimeMillis())
.build();
} else {
return Health.down()
.withDetail("apiUrl", apiBaseUrl)
.withDetail("reason", "API not reachable")
.build();
}
} catch (Exception e) {
// Timeout, cancellation, or any runtime error — report UNKNOWN, never crash
return Health.unknown()
.withDetail("service", "downstream-api")
.withDetail("error", e.getMessage())
.build();
}
}
private boolean checkApiReachability(String baseUrl) {
// In production: use WebClient or RestTemplate with timeout
// For demonstration, assume a ping endpoint exists at /health
try {
// var response = webClient.get().uri("/health").retrieve().toBodilessEntity().block(Duration.ofSeconds(2));
// return response.getStatusCode().is2xxSuccessful();
return true; // Placeholder — replace with actual HTTP call
} catch (Exception e) {
return false;
}
}
}
Pattern 4: Profile-Based Configuration Override
Different environments require different beans, properties, and behaviors. This pattern shows profile-activated configuration classes that selectively override defaults.
package com.example.config.profiles;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
/**
* Production-specific configuration: hardened security settings,
* connection pooling tuning, and monitoring integration.
*/
@Configuration
@Profile("prod")
public class ProductionConfig {
@Bean
public com.zaxxer.hikari.HikariDataSource productionDataSource(
@Value("${spring.datasource.url}") String url,
@Value("${spring.datasource.username}") String username,
@Value("${spring.datasource.password}") String password) {
var config = new com.zaxxer.hikari.HikariDataSource();
config.setJdbcUrl(url);
config.setUsername(username);
config.setPassword(password);
// Production-specific tuning — aggressive pool sizing for high-traffic apps
config.setMaximumPoolSize(20);
config.setMinimumIdle(5);
config.setIdleTimeout(300_000L);
config.setMaxLifetime(1_800_000L);
config.setConnectionTimeout(30_000L);
return config;
}
}
/**
* Development profile: relaxed settings, debug logging, H2 in-memory database.
*/
@Configuration
@Profile("dev")
public class DevelopmentConfig {
@Bean
public com.zaxxer.hikari.HikariDataSource developmentDataSource() {
var config = new com.zaxxer.hikari.HikariDataSource();
config.setJdbcUrl("jdbc:h2:mem:devdb;DB_CLOSE_DELAY=-1");
config.setUsername("sa");
config.setPassword("");
// Development-friendly — small pool, auto-reset on error
config.setMaximumPoolSize(3);
config.setMinimumIdle(1);
return config;
}
}
Constraints
MUST DO
- Use
@ConfigurationProperties(prefix = "...")on records or final classes with@ConstructorBindingconstructors for all externalized config binding - Guard every auto-configuration bean with at least two conditional annotations (
@ConditionalOnClass+@ConditionalOnMissingBean) - Place auto-configuration class FQNs in
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports, never inspring.factories - Implement all Actuator health indicators with timeout protection using
CompletableFuture.orTimeout()— no blocking calls that can hang the health endpoint - Register
@EnableConfigurationPropertieson a configuration class or use@ConfigurationPropertiesScanto auto-scan for properties classes - Use
.yml/.yamlformat for all configuration files containing nested objects, lists, or multi-line values - Validate configuration in record compact constructors with descriptive
IllegalArgumentExceptionmessages
MUST NOT DO
- Annotate
@ConfigurationPropertiesclasses with@Component— they are managed by Spring Boot's property binding mechanism, not component scanning - Use
.propertiesformat for complex nested configurations — use YAML to avoid key duplication and improve readability - Block the health indicator thread — never call blocking I/O (e.g.,
RestTemplate.getForObject()) without wrapping in async execution with a timeout - Hardcode profile names in application logic — resolve them through
@Value("${spring.profiles.active}")injection or constructor dependency onEnvironment - Include more than three active profiles simultaneously — this creates unpredictable precedence conflicts; use
spring.profiles.includefor shared defaults instead
Output Template
When applying this skill, produce outputs following this structure:
- Configuration Model — Typed record/class with
@ConfigurationProperties, compact constructor validation, and corresponding YAML mapping - Auto-Configuration Class — Conditional annotations, bean definitions, and
@AutoConfigurationregistration - Starter Registration File — Complete
AutoConfiguration.importsfile content - Profile-Specific Configuration —
@Profile("name")annotated classes with environment-specific overrides - Actuator Health Indicator — Async health check implementation with timeout and error handling
- Production Pitfalls Section — Common mistakes, anti-patterns, and mitigation strategies
Related Skills
| Skill | Purpose |
|---|---|
spring-security-core |
Security configuration layer that sits on top of auto-configured beans (DataSource for JDBC auth, custom filters) |
spring-data-jpa |
JPA/hibernate auto-configuration with derived query methods, pagination, and transaction management |