@ConfigurationProperties tipado con validación

Avanzado
Spring Boot
Spring Boot
Actualizado: 07/05/2026

Diagrama: tutorial-spring-boot-configuration-properties-tipado

Por qué dejar de usar @Value

@Value("${app.email.host}") funciona, pero arrastra problemas en proyectos grandes:

  • Cada @Value está diseminado por el código, lo que dificulta saber qué propiedades usa la aplicación.
  • No hay validación: si la propiedad no existe, el placeholder queda como ${app.email.host} literal o explota en runtime.
  • No hay autocompletado en el IDE al escribir el nombre.
  • El refactoring es manual: si se renombra app.email.host a app.mail.host, hay que buscar y reemplazar a mano cada @Value.

@ConfigurationProperties resuelve los cuatro problemas. Define una clase tipada que se enlaza a un prefijo de propiedades. Spring Boot la valida en arranque, el IDE autocompleta y el refactoring es de tipos.

Configuración tipada con records

En Java 25 el patrón idiomático combina @ConfigurationProperties con records inmutables:

@ConfigurationProperties(prefix = "app.email")
@Validated
public record EmailProperties(
    @NotBlank String host,
    @Min(1) @Max(65535) int port,
    @NotBlank String username,
    @NotBlank String password,
    boolean tls,
    @DurationMin(seconds = 1) Duration timeout
) {
    // Constructor compacto con valores por defecto si conviene
    public EmailProperties {
        if (timeout == null) timeout = Duration.ofSeconds(10);
    }
}

Y el archivo application.yml:

app:
  email:
    host: smtp.example.com
    port: 587
    username: notifications@example.com
    password: ${SMTP_PASSWORD}
    tls: true
    timeout: 30s

Spring Boot resuelve los placeholders, valida y construye una instancia inmutable. Si falta cualquier campo @NotBlank, la aplicación no arranca: lanza BindValidationException con el detalle de qué falló.

Registrar las clases de configuración

Hay dos formas:

@ConfigurationPropertiesScan en la clase principal

Registra automáticamente todas las clases con @ConfigurationProperties del paquete:

@SpringBootApplication
@ConfigurationPropertiesScan
public class EmpleadosApplication {
    public static void main(String[] args) {
        SpringApplication.run(EmpleadosApplication.class, args);
    }
}

Es la forma más cómoda. Spring Boot busca clases anotadas y las registra como beans automáticamente.

@EnableConfigurationProperties explícito

Útil cuando la clase de configuración no está en el paquete principal o cuando se quiere controlar exactamente qué se registra:

@Configuration
@EnableConfigurationProperties({EmailProperties.class, KafkaProperties.class})
public class AppConfig {}

Inyección y uso

La clase con @ConfigurationProperties se inyecta como cualquier otro bean:

@Service
@RequiredArgsConstructor
@Slf4j
public class EmailSenderService {

    private final EmailProperties props;

    public void enviar(String destinatario, String asunto, String cuerpo) {
        log.info("Enviando email a {} via {}:{} (tls={})",
            destinatario, props.host(), props.port(), props.tls());
        // ...
    }
}

El acceso es por método (porque es un record): props.host() en lugar de props.getHost(). El servicio queda independiente de la fuente de configuración: igual le da que venga de application.yml, de variables de entorno o de Vault.

Jerarquías anidadas

Para configuraciones complejas se anidan records:

@ConfigurationProperties(prefix = "app")
@Validated
public record AppProperties(
    @Valid Email email,
    @Valid Database database,
    @Valid Security security
) {

    public record Email(
        @NotBlank String host,
        @Min(1) @Max(65535) int port,
        @NotBlank String username,
        String password,
        boolean tls
    ) {}

    public record Database(
        @NotBlank String url,
        @NotBlank String username,
        String password,
        @Min(1) int poolSize
    ) {}

    public record Security(
        @NotBlank String jwtSecret,
        @DurationMin(minutes = 1) Duration tokenExpiration,
        List<String> allowedOrigins
    ) {}
}
app:
  email:
    host: smtp.example.com
    port: 587
    username: notifications@example.com
    password: ${SMTP_PASSWORD}
    tls: true
  database:
    url: jdbc:postgresql://localhost:5432/empleados
    username: empleados_user
    password: ${DB_PASSWORD}
    pool-size: 10
  security:
    jwt-secret: ${JWT_SECRET}
    token-expiration: 1h
    allowed-origins:
      - https://app.empresa.com
      - https://admin.empresa.com

@Valid en cada subrecord asegura que la validación se propaga. Spring Boot acepta tanto pool-size (kebab-case en YAML) como poolSize (camelCase) y los enlaza al campo poolSize del record.

Conversiones automáticas

Spring Boot convierte automáticamente entre tipos:

  • Duration: 30s, 5m, 1h, 2d (basado en ISO 8601 o sintaxis simplificada)
  • DataSize: 10MB, 1GB, 512KB
  • List<T>: cualquier YAML array o coma-separado
  • Map<K,V>: bloque YAML con clave-valor
  • Enum: nombre o número del enum (case-insensitive)
  • Path, URL, URI: strings que representan rutas o URLs

Ejemplo:

@ConfigurationProperties(prefix = "app.cache")
public record CacheProperties(
    Duration ttl,
    DataSize maxSize,
    Map<String, Duration> perKeyTtl,
    @NotNull EvictionPolicy evictionPolicy
) {
    public enum EvictionPolicy { LRU, LFU, FIFO }
}
app:
  cache:
    ttl: 10m
    max-size: 100MB
    per-key-ttl:
      empleados: 5m
      departamentos: 1h
    eviction-policy: LRU

Validación en arranque (fail-fast)

@Validated + Bean Validation aborta la aplicación si una propiedad obligatoria falta o tiene un valor inválido. El mensaje de error es explícito:

***************************
APPLICATION FAILED TO START
***************************

Description:

Binding to target [Bindable@67c33749 type = com.empresa.config.EmailProperties]
failed:

    Property: app.email.password
    Value: null
    Reason: must not be blank

Esto es exactamente lo que se quiere en producción: fallar en arranque antes de aceptar tráfico, no fallar en runtime tres horas después.

Anti-patrón habitual: validar la configuración con código defensivo en cada uso (if (props.host() == null) throw new IllegalStateException()). Mejor declarativo con anotaciones de validación; se aplica una vez en arranque y queda fuera del código de negocio.

Constructor compacto para defaults y normalización

Cuando hace falta un valor por defecto o una normalización, el constructor compacto del record es el lugar:

@ConfigurationProperties(prefix = "app.api")
@Validated
public record ApiProperties(
    @NotBlank String baseUrl,
    Duration timeout,
    int maxRetries,
    @Valid Pagination pagination
) {
    public ApiProperties {
        // Normalización: quitar slash final
        if (baseUrl != null && baseUrl.endsWith("/")) {
            baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
        }
        // Defaults
        if (timeout == null) timeout = Duration.ofSeconds(30);
        if (maxRetries < 0) maxRetries = 3;
        if (pagination == null) pagination = new Pagination(20, 100);
    }

    public record Pagination(int defaultSize, int maxSize) {}
}

Metadata para autocompletado

Spring Boot genera autocompletado en IDEs leyendo el archivo META-INF/spring-configuration-metadata.json. Para que se genere automáticamente, hay que añadir el processor:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

Con esto, escribiendo app.email. en application.yml desde IntelliJ IDEA o VS Code, el IDE sugiere host, port, username, etc., con su tipo y descripción.

Para añadir descripciones que aparezcan en el autocompletado, hay que documentar los componentes del record con Javadoc (Spring Boot 3.4+ lo lee):

/**
 * Configuración del servidor SMTP para notificaciones por email.
 *
 * @param host servidor SMTP, sin esquema
 * @param port puerto del servidor SMTP, típicamente 25, 465 o 587
 * @param tls activa STARTTLS sobre la conexión
 */
@ConfigurationProperties(prefix = "app.email")
public record EmailProperties(
    @NotBlank String host,
    @Min(1) @Max(65535) int port,
    @NotBlank String username,
    String password,
    boolean tls
) {}

Testing de @ConfigurationProperties

Hay dos enfoques:

Test aislado con ApplicationContextRunner

Carga solo la configuración propia, sin levantar el contexto Spring completo:

class EmailPropertiesTest {

    private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
        .withUserConfiguration(EmailPropertiesConfig.class);

    @Test
    void cargaDesdeYaml() {
        contextRunner
            .withPropertyValues(
                "app.email.host=smtp.test.com",
                "app.email.port=587",
                "app.email.username=user",
                "app.email.password=pass",
                "app.email.tls=true"
            )
            .run(context -> {
                assertThat(context).hasSingleBean(EmailProperties.class);
                var props = context.getBean(EmailProperties.class);
                assertThat(props.host()).isEqualTo("smtp.test.com");
                assertThat(props.port()).isEqualTo(587);
                assertThat(props.tls()).isTrue();
            });
    }

    @Test
    void fallaSiFaltaHost() {
        contextRunner
            .withPropertyValues(
                "app.email.port=587",
                "app.email.username=user",
                "app.email.password=pass"
            )
            .run(context -> {
                assertThat(context).hasFailed();
                assertThat(context.getStartupFailure())
                    .hasRootCauseInstanceOf(BindValidationException.class)
                    .hasMessageContaining("must not be blank");
            });
    }

    @EnableConfigurationProperties(EmailProperties.class)
    static class EmailPropertiesConfig {}
}

Este test es muy rápido (no carga JPA, ni web, ni nada más) y verifica el contrato de configuración.

Test con @SpringBootTest y @ActiveProfiles

Más pesado pero útil para validar el yaml de un perfil completo:

@SpringBootTest
@ActiveProfiles("test")
class AppPropertiesIntegrationTest {

    @Autowired AppProperties props;

    @Test
    void cargaConfigDeTest() {
        assertThat(props.email().host()).isEqualTo("localhost");
        assertThat(props.database().url()).startsWith("jdbc:h2:mem:");
    }
}

Patrón de records inmutables vs clase mutable

Los records hacen que la configuración sea inmutable. Esto es deseable: la configuración no debe cambiar en runtime. Si por alguna razón se necesita configuración mutable (refresh dinámico con Spring Cloud Config Server), entonces una clase con @ConfigurationProperties y setters sigue siendo válida:

@ConfigurationProperties(prefix = "app.email")
@Setter
@Getter
@Validated
public class EmailProperties {

    @NotBlank private String host;
    @Min(1) @Max(65535) private int port;
    private String username;
    private String password;
    private boolean tls;
}

Pero la regla por defecto es records inmutables: lo que cambia entre entornos viene resuelto en arranque, no en runtime.

Buenas prácticas

  • Un record por dominio funcional: EmailProperties, KafkaProperties, SecurityProperties. Evitar megaclases con cien campos.
  • Prefijo claro: app.{dominio} para configuración propia, evitar nombres genéricos como config.*.
  • @Validated siempre: el coste de añadir la anotación es cero, el beneficio en producción es enorme.
  • Defaults razonables en el constructor compacto: que la aplicación arranque con la mínima configuración posible en local.
  • Documentar con Javadoc: los IDEs lo muestran en autocompletado. Una propiedad sin documentar es una propiedad que nadie sabe para qué sirve seis meses después.

Combinar @ConfigurationProperties tipado + records de Java 25 + @Validated + profiles produce configuración robusta, autocompletada y validada. Es uno de los patrones de mayor retorno por mínimo esfuerzo en proyectos Spring Boot serios.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Spring Boot es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Spring Boot

Explora más contenido relacionado con Spring Boot y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Modelar configuración con @ConfigurationProperties y records de Java 25. Validar valores en arranque con Bean Validation. Definir jerarquías anidadas y colecciones. Generar metadata.json para autocompletado. Probar la configuración con tests aislados.