Conceptos clave: configuración externa en Spring Boot
En microservicios, la configuración debe poder cambiar entre entornos (local, dev, qa, prod) sin recompilar ni redeployar artefactos. Spring Boot ofrece un modelo de configuración basado en Property Sources (fuentes de propiedades), perfiles y una jerarquía de precedencia que permite combinar archivos, variables de entorno y parámetros de arranque.
Property Sources y jerarquía de precedencia
Spring Boot agrega propiedades desde múltiples fuentes y, cuando una clave se repite, gana la de mayor precedencia. En la práctica, esto permite definir valores por defecto en archivos y sobreescribirlos en despliegue con variables de entorno o argumentos.
- Argumentos de línea de comandos:
--server.port=8081 - Variables de entorno:
SERVER_PORT=8081 - Propiedades del sistema:
-Dserver.port=8081 - Archivos de configuración:
application.yml,application-{profile}.yml - Valores por defecto en el código (por ejemplo, en
@Valueo valores iniciales de un POJO)
Regla práctica: usa archivos para defaults y estructura; usa variables de entorno para sobreescrituras por entorno; usa secretos fuera del repositorio.
Perfiles (profiles) para segmentar por entorno
Los perfiles activan bloques de configuración específicos. Puedes activarlos con:
- Variable de entorno:
SPRING_PROFILES_ACTIVE=dev - Argumento:
--spring.profiles.active=dev - En archivo:
spring.profiles.active: dev(menos recomendable para despliegue)
Convención recomendada: local, dev, qa, prod. Evita perfiles “por persona” y prioriza perfiles “por capacidad” cuando aplique (por ejemplo, kafka, redis), combinables con el entorno: SPRING_PROFILES_ACTIVE=prod,kafka.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
Estructura de archivos por entorno (YAML) sin duplicación excesiva
Patrón recomendado: define en application.yml los valores base (comunes) y en application-{profile}.yml solo lo que cambia. Ejemplo:
# application.yml (base, común a todos los entornos) spring: application: name: orders-service server: port: 8080 app: http: connect-timeout: 2s read-timeout: 3s concurrency: max-in-flight: 200 features: enable-new-checkout: false datasource: url: jdbc:postgresql://localhost:5432/orders username: orders password: ${DB_PASSWORD:changeme}# application-dev.yml (solo diferencias) app: features: enable-new-checkout: true datasource: url: jdbc:postgresql://dev-db:5432/orders# application-prod.yml (solo diferencias) server: port: 8080 app: concurrency: max-in-flight: 500 datasource: url: jdbc:postgresql://prod-db:5432/ordersObserva el uso de placeholders: ${DB_PASSWORD:changeme} toma el valor de la variable de entorno DB_PASSWORD y, si no existe, usa un default (útil en local; en prod normalmente se exige que exista).
Importación de configuración (config tree / archivos externos)
Para mantener consistencia y evitar duplicación, es común centralizar “base” en un archivo externo montado en el contenedor/VM y que cada servicio lo importe. En Spring Boot puedes usar spring.config.import:
# application.yml spring: config: import: optional:file:/etc/mycompany/common.ymlEsto permite compartir convenciones (timeouts, límites, toggles) sin copiar/pegar entre repositorios. Mantén el archivo común libre de secretos.
Guía práctica paso a paso: parametrizar conexiones, timeouts, concurrencia y feature flags
Paso 1: define un namespace propio (evita mezclar con propiedades de Spring)
Usa un prefijo como app.* o company.* para propiedades de negocio/infra propias. Esto reduce colisiones y mejora la legibilidad.
Paso 2: modela la configuración con @ConfigurationProperties
En lugar de dispersar @Value, agrupa propiedades en clases tipadas. Ejemplo:
package com.example.orders.config; import java.time.Duration; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "app") public class AppProperties { private Http http = new Http(); private Concurrency concurrency = new Concurrency(); private Features features = new Features(); public Http getHttp() { return http; } public void setHttp(Http http) { this.http = http; } public Concurrency getConcurrency() { return concurrency; } public void setConcurrency(Concurrency concurrency) { this.concurrency = concurrency; } public Features getFeatures() { return features; } public void setFeatures(Features features) { this.features = features; } public static class Http { private Duration connectTimeout = Duration.ofSeconds(2); private Duration readTimeout = Duration.ofSeconds(3); public Duration getConnectTimeout() { return connectTimeout; } public void setConnectTimeout(Duration connectTimeout) { this.connectTimeout = connectTimeout; } public Duration getReadTimeout() { return readTimeout; } public void setReadTimeout(Duration readTimeout) { this.readTimeout = readTimeout; } } public static class Concurrency { private int maxInFlight = 200; public int getMaxInFlight() { return maxInFlight; } public void setMaxInFlight(int maxInFlight) { this.maxInFlight = maxInFlight; } } public static class Features { private boolean enableNewCheckout = false; public boolean isEnableNewCheckout() { return enableNewCheckout; } public void setEnableNewCheckout(boolean enableNewCheckout) { this.enableNewCheckout = enableNewCheckout; } } }Registra el binding (según tu versión):
package com.example.orders; import com.example.orders.config.AppProperties; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; @SpringBootApplication @EnableConfigurationProperties(AppProperties.class) public class OrdersApplication { public static void main(String[] args) { SpringApplication.run(OrdersApplication.class, args); } }Paso 3: aplica la configuración en componentes (ejemplos típicos)
Timeouts HTTP (ejemplo con RestTemplateBuilder):
import com.example.orders.config.AppProperties; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; @Configuration public class HttpClientConfig { @Bean RestTemplate restTemplate(RestTemplateBuilder builder, AppProperties props) { return builder .setConnectTimeout(props.getHttp().getConnectTimeout()) .setReadTimeout(props.getHttp().getReadTimeout()) .build(); } }Límites de concurrencia (ejemplo con un Semaphore para “in-flight requests”):
import java.util.concurrent.Semaphore; import com.example.orders.config.AppProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class ConcurrencyConfig { @Bean Semaphore inFlightSemaphore(AppProperties props) { return new Semaphore(props.getConcurrency().getMaxInFlight()); } }Feature flags (ejemplo simple):
import com.example.orders.config.AppProperties; import org.springframework.stereotype.Service; @Service public class CheckoutService { private final AppProperties props; public CheckoutService(AppProperties props) { this.props = props; } public String checkout() { if (props.getFeatures().isEnableNewCheckout()) { return "NEW"; } return "LEGACY"; } }Patrón recomendado para flags: nombres positivos (enableX), defaults seguros (false) y cambios por entorno mediante variables o archivos de perfil.
Variables de entorno: convenciones y mapeo
En contenedores y plataformas de orquestación, las variables de entorno son el mecanismo más común. Spring Boot mapea variables a propiedades usando equivalencias como:
APP_HTTP_CONNECT_TIMEOUT=2s→app.http.connect-timeoutAPP_CONCURRENCY_MAX_IN_FLIGHT=300→app.concurrency.max-in-flightSPRING_DATASOURCE_URL=...→spring.datasource.url
Recomendación: estandariza nombres en mayúsculas con guiones bajos para despliegue, pero mantén en YAML el formato con puntos y kebab-case.
Validación de configuración al arranque (fail fast)
En microservicios, es preferible fallar al inicio si la configuración es inválida (por ejemplo, timeouts negativos, límites de concurrencia en cero, URLs vacías). Esto evita comportamientos erráticos en runtime.
Opción A: Bean Validation en @ConfigurationProperties
Agrega validaciones con anotaciones y marca la clase con @Validated. Ejemplo:
import jakarta.validation.Valid; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.validation.annotation.Validated; import java.time.Duration; @Validated @ConfigurationProperties(prefix = "app") public class AppProperties { @Valid private Http http = new Http(); @Valid private Concurrency concurrency = new Concurrency(); public Http getHttp() { return http; } public void setHttp(Http http) { this.http = http; } public Concurrency getConcurrency() { return concurrency; } public void setConcurrency(Concurrency concurrency) { this.concurrency = concurrency; } public static class Http { @NotNull private Duration connectTimeout; @NotNull private Duration readTimeout; public Duration getConnectTimeout() { return connectTimeout; } public void setConnectTimeout(Duration connectTimeout) { this.connectTimeout = connectTimeout; } public Duration getReadTimeout() { return readTimeout; } public void setReadTimeout(Duration readTimeout) { this.readTimeout = readTimeout; } } public static class Concurrency { @Min(1) private int maxInFlight; public int getMaxInFlight() { return maxInFlight; } public void setMaxInFlight(int maxInFlight) { this.maxInFlight = maxInFlight; } } }Para que falle temprano, asegúrate de tener el starter de validación en dependencias (spring-boot-starter-validation). Si una propiedad requerida falta o viola una restricción, el contexto no levantará.
Opción B: validación programática (reglas de negocio)
Para reglas más complejas (por ejemplo, readTimeout >= connectTimeout), valida en un bean que se ejecute al inicio:
import com.example.orders.config.AppProperties; import org.springframework.boot.ApplicationRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class StartupValidationConfig { @Bean ApplicationRunner validateConfig(AppProperties props) { return args -> { if (props.getHttp().getReadTimeout().compareTo(props.getHttp().getConnectTimeout()) < 0) { throw new IllegalStateException("app.http.read-timeout debe ser >= app.http.connect-timeout"); } }; } }Patrón: lanza excepciones claras y accionables, indicando la clave exacta que debe corregirse.
Separación: configuración no sensible vs secretos
Qué considerar “secreto”
- Contraseñas, tokens, API keys, client secrets
- Certificados privados, claves de cifrado
- URLs con credenciales embebidas
Patrones recomendados
- No sensibles en repositorio: timeouts, tamaños de pool, límites, flags, endpoints sin credenciales.
- Secretos fuera del repositorio: inyectados como variables de entorno, archivos montados (volúmenes) o proveedores de secretos.
- Evita defaults inseguros en prod: para secretos, prefiere “sin default” y falla al arranque si faltan.
Ejemplo: exigir contraseña en entornos no locales:
# application.yml datasource: password: ${DB_PASSWORD:} app: env: ${APP_ENV:local}import org.springframework.boot.ApplicationRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class SecretsValidation { @Bean ApplicationRunner validateSecrets(org.springframework.core.env.Environment env) { return args -> { String appEnv = env.getProperty("app.env", "local"); String dbPassword = env.getProperty("DB_PASSWORD", ""); if (!"local".equals(appEnv) && (dbPassword == null || dbPassword.isBlank())) { throw new IllegalStateException("Falta DB_PASSWORD para entorno " + appEnv); } }; } }Consistencia entre entornos sin duplicación: patrones operativos
Matriz de configuración: base + overlays
Define una matriz clara de qué se configura dónde:
| Tipo | Ubicación recomendada | Ejemplos |
|---|---|---|
| Defaults comunes | application.yml | timeouts base, límites base, flags por defecto |
| Diferencias por entorno | application-dev.yml, application-prod.yml | URLs de servicios, tamaños de concurrencia |
| Overwrites por despliegue | Variables de entorno / args | puertos, toggles temporales, endpoints específicos |
| Secretos | Proveedor de secretos / env / archivos montados | passwords, tokens, keys |
Convenciones de nombres y documentación mínima
- Prefijo único:
app.* - Unidades explícitas: usa
Durationcon sufijos (200ms,2s) y tamaños con unidades cuando aplique. - Documenta en el repositorio un “catálogo” de propiedades (tabla) con: clave, tipo, default, rango válido, si es secreto.
Evitar “drift” entre entornos
- Minimiza diferencias: cambia solo lo necesario (capacidad, endpoints, credenciales).
- Reutiliza el mismo artefacto: el jar/imagen debe ser idéntico entre entornos; solo cambia la configuración.
- Valida al arranque: si falta una propiedad crítica o está fuera de rango, el servicio no debe iniciar.
Ejemplo de catálogo de propiedades (plantilla)
| Clave | Tipo | Default | Rango/Regla | Secreto |
|---|---|---|---|---|
app.http.connect-timeout | Duration | 2s | > 0 | No |
app.http.read-timeout | Duration | 3s | >= connect-timeout | No |
app.concurrency.max-in-flight | int | 200 | >= 1 | No |
app.features.enable-new-checkout | boolean | false | n/a | No |
DB_PASSWORD | string | (vacío) | requerido en no-local | Sí |