# Spring Boot Auto Config

> Implements Spring Boot auto-configuration mechanics, externalized config binding with @ConfigurationProperties, custom starter development with conditional annotations, and Actuator endpoint customization for production-ready applications.

- Skill: `paulpas/spring-boot-auto-config` (Agent Skill)
- Install (CLI): `npx skillmds@latest add paulpas/spring-boot-auto-config`
- Raw SKILL.md: https://api.skillmd.com/api/skills/paulpas/spring-boot-auto-config/raw
- Safety review: PASS (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: paulpas (https://skillmd.com/u/paulpas)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/paulpas/spring-boot-auto-config

---






# 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 `@ConfigurationProperties` are 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 `@ConditionalOnProperty` guard all auto-config beans
- [ ] Check that Actuator health indicators implement `HealthIndicator` interface with meaningful UP/DOWN status
- [ ] Validate profile activation uses `spring.profiles.active` or `spring.profiles.include` in application.yml
- [ ] Confirm externalized config uses `.yml` or `.yaml` format (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 `@Configuration` class instead
- Direct database or security configuration — use `spring-data-jpa` or `spring-security-core` skills
- 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 `@ConfigurationProperties` classes

---

## Core Workflow

1. **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 `@ConstructorBinding` constructors for complex hierarchies. Register the bean in a `@Configuration` class 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 for `org.springframework.boot.context.properties.ConfigurationProperties`.

2. **Guard Beans with Conditional Annotations** — Wrap auto-configuration beans using `@ConditionalOnClass`, `@ConditionalOnMissingBean`, `@ConditionalOnProperty`, and `@ConditionalOnWebApplication` to 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`).

3. **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 deprecated `spring.factories` approach used in Spring Boot 2.x. The starter POM should have `scope=compile` for runtime dependencies and `scope=provided` for API dependencies like Jackson or Hibernate.

   **Checkpoint:** After building the starter JAR, inspect it with `jar tf your-starter.jar | grep AutoConfiguration.imports` to confirm the file exists. Verify no entries appear in `META-INF/spring.factories`. Run a test app that depends on the starter and confirm the auto-config triggers without explicit `@Import`.

4. **Activate Profiles Strategically** — Configure environment-specific overrides using profile-activated `@Configuration` classes annotated with `@Profile("profile-name")`. In `application.yml`, use `spring.profiles.active: dev,local` for active profiles and `spring.profiles.include: common` for always-loaded defaults. Never hardcode profile names in application logic — resolve them via `Environment#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 (`--debug` flag).

5. **Implement Custom Actuator Health Indicator** — Create a class implementing `org.springframework.boot.actuate.health.HealthIndicator`. Implement the `health()` method to check downstream dependencies (database, cache, message broker) and return either `Health.up().withDetail("key", "value").build()` or `Health.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/health` endpoint must return a JSON object with your component's name as a key. Ensure health checks are non-blocking and include timeout logic (use `CompletableFuture.supplyAsync()` with `.orTimeout(3, TimeUnit.SECONDS)`). Verify that an `OutOfMemoryError` or unhandled exception in the health check does not crash the application — wrap all health checks in try/catch returning `Health.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.

```java
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");
        }
    }
}
```

```yaml
# 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:** `@ConfigurationProperties` on 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.

```java
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());
    }
}
```

```properties
# 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.

```java
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.

```java
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 `@ConstructorBinding` constructors 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 in `spring.factories`
- Implement all Actuator health indicators with timeout protection using `CompletableFuture.orTimeout()` — no blocking calls that can hang the health endpoint
- Register `@EnableConfigurationProperties` on a configuration class or use `@ConfigurationPropertiesScan` to auto-scan for properties classes
- Use `.yml` / `.yaml` format for all configuration files containing nested objects, lists, or multi-line values
- Validate configuration in record compact constructors with descriptive `IllegalArgumentException` messages

### MUST NOT DO
- Annotate `@ConfigurationProperties` classes with `@Component` — they are managed by Spring Boot's property binding mechanism, not component scanning
- Use `.properties` format 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 on `Environment`
- Include more than three active profiles simultaneously — this creates unpredictable precedence conflicts; use `spring.profiles.include` for shared defaults instead

---

## Output Template

When applying this skill, produce outputs following this structure:

1. **Configuration Model** — Typed record/class with `@ConfigurationProperties`, compact constructor validation, and corresponding YAML mapping
2. **Auto-Configuration Class** — Conditional annotations, bean definitions, and `@AutoConfiguration` registration
3. **Starter Registration File** — Complete `AutoConfiguration.imports` file content
4. **Profile-Specific Configuration** — `@Profile("name")` annotated classes with environment-specific overrides
5. **Actuator Health Indicator** — Async health check implementation with timeout and error handling
6. **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 |

---

## Live References

1. [Spring Boot Auto-Configuration Documentation](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.external-config.typesafe-configuration-properties)
2. [@ConfigurationProperties Reference](https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/context/properties/ConfigurationProperties.html)
3. [Conditional Annotations Guide](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.feature-auto-dependency-conditionals)
4. [Spring Boot Starter Auto-Configuration Import File](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.feature-spring-application-auto-configuration.imports)
5. [Actuator Health Indicators API](https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/actuate/health/HealthIndicator.html)
6. [Spring Profiles Documentation](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.feature-features.profiles)
7. [Externalized Configuration Overview](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.external-config)

