spring-boot-config-refactor
Refactor a Spring Boot application's configuration from legacy application.properties and scattered @Value usage to structured YAML and immutable @ConfigurationProperties records. Use this when migrating messy configuration, standardizing property naming, introducing app-prefixed custom properties,
Install
mkdir -p .claude/skills/spring-boot-config-refactor && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/13395" && unzip -o skill.zip -d .claude/skills/spring-boot-config-refactor && rm skill.zipInstalls to .claude/skills/spring-boot-config-refactor
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Refactor a Spring Boot application's configuration from legacy application.properties and scattered @Value usage to structured YAML and immutable @ConfigurationProperties records. Use this when migrating messy configuration, standardizing property naming, introducing app-prefixed custom properties, or separating sensible defaults from local development overrides.About this skill
Spring Boot configuration refactor skill
Use this skill when the task is to analyze, restructure, and migrate application configuration in a Spring Boot codebase.
This skill is specifically optimized for repositories where:
application.propertieshas grown organically over time and is hard to understand.- Local development uses a
devprofile and an additional config location such asdevops/dev/config/. - Test and production deployments are configured externally through Kubernetes
ConfigMap/Secret/sealed secrets rather than profile-specific packaged files. - Many settings are injected via
@Valueannotations. - The goal is to move to type-safe, immutable, record-based configuration using
@ConfigurationProperties.
Desired target state
Follow these rules unless the user explicitly instructs otherwise:
- Use YAML instead of
.propertiesfor Spring configuration. - Put application-specific properties under the
appprefix. - Keep standard Spring Boot / framework / library properties under their normal prefixes (for example
spring.*,management.*,server.*,logging.*). - Keep the same conceptual split between:
application.ymlfor minimal defaults and shared base configuration.application-dev.ymlfor local development overrides.application-test.ymlfor Spring Boot test scenarios when relevant.
- Migrate from scattered
@Valueusage to@ConfigurationPropertieswith immutable Java records. - Inject configuration records as Spring beans via constructor injection.
- Add validation so invalid configuration fails fast during startup.
- Preserve the existing deployment model where production/test environment overrides come from external configuration, Kubernetes config maps, and secrets.
- Do not introduce environment-specific packaged config for production unless the user explicitly asks for it.
Key implementation principles
Configuration layout
src/main/resources/application.yml- Keep it minimal.
- Include sensible defaults only.
- Do not hardcode environment-specific secrets or infrastructure endpoints.
- Use Spring Boot's native property keys directly for framework properties (
spring.activemq.*,spring.data.redis.*, etc.) rather than creating intermediate "bridge" property layers (e.g.activemq.broker.url→spring.activemq.broker-url). Set localhost/empty defaults inline and let K8s ConfigMaps override with real values.
src/main/resources/application-dev.yml- Use for local dev defaults and local overrides only if that matches the repo's current practice.
- If the project already loads external files from
devops/dev/config/usingspring.config.additional-location, preserve that approach unless the user asks to change it.
src/test/resources/application-test.yml- Use when test-specific configuration is needed.
Property modeling
- Group custom application properties into one or more records under a dedicated package such as:
...config...config.properties- or the package already used by the project for application config
- Prefer a small number of well-structured top-level property records over many tiny fragmented ones.
- Use nested records to mirror the YAML hierarchy.
- Keep names domain-oriented and self-explanatory.
- Multi-module projects: each Gradle/Maven module that needs config should have its own
@ConfigurationPropertiesrecord bound to its relevant subtree. For example, a mainappmodule getsAppProperties(prefix = "app"), while anintegration-foomodule getsFooProperties(prefix = "app.integration.foo"). This keeps each module self-contained and avoids forcing a dependency on the root config record.
Example pattern:
package com.example.app.config.properties;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
@Validated
@ConfigurationProperties(prefix = "app")
public record AppProperties(
@Valid Feature feature,
@Valid Integration integration
) {
public record Feature(
boolean enabled,
@Min(1) int batchSize
) {}
public record Integration(
@NotBlank String baseUrl,
@NotNull Timeout timeout
) {
public record Timeout(
@Min(1) int connectSeconds,
@Min(1) int readSeconds
) {}
}
}
Spring Boot wiring
- Prefer
@ConfigurationPropertiesScanon the main application class if it is not already in use. - Alternatively use
@EnableConfigurationProperties(...)only when there is a strong repository-specific reason (for example, test@Configurationclasses that don't trigger@SpringBootTestauto-scanning — see the test pitfalls section). - Use constructor injection exclusively.
- Access values using record accessors, for example
appProperties.integration().baseUrl(). - Dependency:
@Validatedon@ConfigurationPropertiesrecords requiresspring-boot-starter-validation. Check if it is already present before adding it.
Validation
- Use
@Validatedon the configuration record. - Use Jakarta Bean Validation annotations such as:
@NotNull@NotBlank@Min@Max@Positive@Validfor nested records
- Ensure configuration errors fail fast at application startup.
@Validon nested records does NOT enforce non-null:@Valid Security securitymeans validation runs on the nested record's contents only if the record is non-null. If the property subtree is absent, the component isnulland@Valid nullpasses silently. At runtime, accessingsecurity().hashSalt()then throwsNullPointerException. Use@NotNull @Validif the component must always be present, or accept that the component may be null and guard access accordingly.
Handling existing @Value usage
Auditing @Value annotations
- Search the codebase for all
@Valueannotations — but distinguish Lombok@Valuefrom Spring@Value. Lombok@Valueis a class-level annotation (generates getters, equals, hashCode). Spring@Valueisorg.springframework.beans.factory.annotation.Valueand is used on fields/parameters for property injection. Filter for the Spring import, not just@Valuetext matches. - Also search for
@Scheduled,@JmsListener,@KafkaListener, and other annotations that use${...}placeholders in their attributes (e.g.@Scheduled(cron = "${my.cron}"),@JmsListener(destination = "${my.queue}")). These are property references that will break when legacy keys are removed, but they do not show up in a@Valuesearch.
Classifying properties
Classify each occurrence:
- custom application property → migrate under
app.* - framework/library property → usually leave as framework config and inject a richer Spring abstraction where possible
- literal default with occasional override need → model as a typed property with a sensible default in YAML, or keep optional via constructor defaulting only if clearly justified
Migrating classes
- Replace field injection and
@Valueinjection with constructor-injected configuration beans. - When using Lombok
@RequiredArgsConstructorfor constructor injection, be aware that@RequiredArgsConstructordoes not propagate@Qualifierannotations. If a bean needs both a@ConfigurationPropertiesrecord and a@Qualifier-annotated bean (e.g. a namedRestClient), keep@Autowired @Qualifieron the qualified field (non-final) and make only the config properties fieldfinalfor constructor injection. - Remove dead properties after migration.
Bridge properties and infrastructure config
Legacy configurations often have "bridge" properties — custom intermediate property keys that are referenced by Spring Boot's native property placeholders. For example:
# Bridge pattern (avoid in target state):
redis:
host: 127.0.0.1
port: 6379
spring:
data:
redis:
host: "${redis.host}"
port: "${redis.port}"
During migration:
- Identify all bridge properties during the audit phase. Common examples:
db.*→spring.datasource.*,activemq.broker.*→spring.activemq.*,redis.*→spring.data.redis.*. - Replace bridges with direct values in
application.yml. Set localhost/empty defaults forspring.activemq.*,spring.data.redis.*, etc. directly, and let K8s ConfigMaps override using the standard Spring property names (or their env var equivalents via Spring Boot's relaxed binding, e.g.SPRING_ACTIVEMQ_BROKER_URL). - If a bridge cannot be removed immediately (e.g.
db.*properties are used to composespring.datasource.urlvia${db.server}:${db.port}/${db.name}), keep it temporarily and document it as a follow-up cleanup.
Property naming rules
- Application-specific keys must move under
app. - Avoid flat names when the domain has hierarchy.
- Prefer this:
app.integration.certificate-service.base-urlapp.jobs.cleanup.batch-size
- Avoid this:
certificateServiceUrlcleanupBatch
Secrets and external config
- Never hardcode secrets in repository defaults.
- Assume sensitive values should come from environment variables, sealed secrets, or external config in the devops repository.
- Prefer env var placeholders for secrets where possible, for example
app.security.hash-salt: "${HASH_SALT:}". - Preserve compatibility with
spring.config.additional-locationif the project already relies on it.
Required workflow — phased migration
When using this skill, follow this phased workflow. Each phase should result in a passing test suite and a separate commit. The user can review, test, and roll back each phase independently.
Phase 1: Audit the current state
Inspect at leas
Content truncated.