Spring Boot y Spring Cloud
Spring Cloud para microservicios: Config Server, discovery, gateway y resiliencia
Avanzado9 min de lecturaActualizado
Esta página se tradujo del inglés con ayuda de IA y se cotejó con el original. Leer la versión en inglés
Sabrás explicar qué problemas de sistemas distribuidos resuelve Spring Cloud y qué proyectos se encargan de ellos hoy: ejecutar un Config Server y refrescar a sus clientes, registrar servicios en Eureka y llamarlos por su nombre a través de un RestClient con balanceo de carga, enrutar el tráfico externo con Spring Cloud Gateway y proteger las llamadas con circuit breakers de Resilience4j, todo sobre Spring Boot 3 con los release trains 2023 y 2024.
Lo que aprenderás
- Relacionar los problemas recurrentes de una arquitectura de microservicios con los proyectos de Spring Cloud que los abordan
- Montar Spring Cloud Config Server con un backend Git y refrescar la configuración de los clientes en caliente
- Registrar servicios en Eureka y llamarlos por su nombre lógico mediante un RestClient con balanceo de carga o una HTTP interface
- Definir rutas, predicates y filters en Spring Cloud Gateway
- Envolver las llamadas remotas en circuit breakers, retries y time limiters de Resilience4j y explicar cuándo ayuda cada uno
- Identificar los componentes retirados de Spring Cloud Netflix y sus reemplazos actuales
Dividir una aplicación en una docena de servicios sustituye problemas que ya tenías resueltos (una llamada a un método, un fichero de configuración compartido, un único log) por otros que todavía no: dónde está el otro servicio, cómo obtiene su configuración, qué pasa cuando va lento y cómo llega el tráfico externo al servicio correcto. Spring Cloud es el paraguas que agrupa los proyectos de Spring que resuelven esos problemas sobre Spring Boot. Ha cambiado mucho desde la primera generación basada en Netflix, así que esta página describe el panorama actual para Spring Boot 3 y muestra lo esencial de cada pieza: configuración, discovery, balanceo de carga, gateway y resiliencia.
Release trains y el catálogo actual
Spring Cloud no es una sola librería, sino un conjunto coordinado de proyectos que se publican juntos como un release train, cuya versión se importa como un BOM:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2024.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Cada train está pensado para una línea menor de Spring Boot; combinar un train con la versión equivocada de Boot provoca fallos crípticos durante el arranque. Los proyectos con los que más te cruzarás:
| Problema | Proyecto | Notas |
|---|---|---|
| Configuración centralizada y versionada | Spring Cloud Config | Backends Git, Vault, JDBC y de ficheros |
| Registro de servicios | Spring Cloud Netflix Eureka, Consul, Zookeeper, Kubernetes | Eureka es el único componente de Netflix que queda |
| Balanceo de carga en el cliente | Spring Cloud LoadBalancer | Sustituyó a Ribbon |
| Clientes HTTP declarativos | Spring Cloud OpenFeign (mantenimiento), HTTP interfaces de Spring Framework | Ambos se integran con LoadBalancer |
| API gateway | Spring Cloud Gateway | Sustituyó a Zuul; núcleo reactivo, con una variante servlet |
| Circuit breaking | Spring Cloud Circuit Breaker con Resilience4j | Sustituyó a Hystrix |
| Mensajería orientada a eventos | Spring Cloud Stream | Binders para Kafka, RabbitMQ y otros |
| Difusión de la configuración | Spring Cloud Bus | Propaga los eventos de refresh a través de un broker |
| Contratos dirigidos por el consumidor | Spring Cloud Contract | Stubs generados a partir de los contratos |
El tracing distribuido salió de Spring Cloud cuando se retiró Sleuth; ahora lo proporciona Micrometer Tracing dentro del propio Boot, como se explica en la guía de observabilidad.
Configuración centralizada
Doce servicios con doce ficheros application.yml, todos con el mismo host de base de datos y la misma dirección del broker de mensajería, acaban divergiendo en pocas semanas. Spring Cloud Config Server sirve la configuración desde un backend versionado, casi siempre un repositorio Git, indexada por nombre de aplicación, perfil y label (rama o tag):
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication { /* main */ }
server:
port: 8888
spring:
cloud:
config:
server:
git:
uri: https://git.example.com/platform/config.git
default-label: main
search-paths: '{application}' # one folder per service
clone-on-start: true
El repositorio contiene application.yml (compartido por todos), orders.yml, orders-prod.yml, etc.; GET /orders/prod/main devuelve las property sources combinadas del servicio orders en el perfil prod. Los valores cifrados pueden guardarse como {cipher}... y el servidor los descifra con una clave simétrica o un par de claves, lo que evita tener secretos en texto plano en Git sin necesidad de un vault aparte, aunque Spring Cloud Vault o un gestor de secretos en la nube es la opción más sólida.
Los clientes añaden spring-cloud-starter-config y apuntan al servidor mediante el mecanismo de config import disponible desde Boot 2.4:
spring:
application:
name: orders # becomes {application} in the request
config:
import: "optional:configserver:http://config:8888"
cloud:
config:
fail-fast: true # refuse to start without configuration in production
retry:
max-attempts: 6
El antiguo enfoque con bootstrap.yml sigue existiendo detrás de spring-cloud-starter-bootstrap, pero es legacy; spring.config.import es el modelo actual y se integra con el orden de precedencia de propiedades descrito en la guía de configuración.
Cambiar un valor en Git no afecta a un servicio en ejecución hasta que vuelve a leer su configuración. Expón el endpoint refresh de Actuator y hazle un POST: Boot vuelve a hacer el binding de los beans @ConfigurationProperties y recrea los beans anotados con @RefreshScope:
@RefreshScope
@Service
class PricingService {
private final BigDecimal surcharge;
PricingService(@Value("${pricing.surcharge}") BigDecimal surcharge) {
this.surcharge = surcharge; // a new instance is created on the next call after a refresh
}
}
Refrescar cada instancia a mano no escala; Spring Cloud Bus conecta los servicios a Kafka o RabbitMQ para que un único POST /actuator/busrefresh en cualquier instancia (o un webhook del proveedor Git al endpoint /monitor del Config Server) llegue a todas. @RefreshScope envuelve el bean en un proxy, lo que cuesta un poco en cada llamada y encaja mal con @Scheduled y con los beans que mantienen conexiones; para la mayoría de los ajustes refrescables, prefiere @ConfigurationProperties, que se vuelven a enlazar in situ.
Service discovery y balanceo de carga
Dejar http://orders-2.internal:8080 escrito a fuego en el código falla la primera vez que se sustituye una instancia. Un registro de servicios permite que cada instancia registre su nombre y su dirección al arrancar, envíe heartbeats y se dé de baja al apagarse; los clientes piden al registro las instancias actuales de orders y eligen una.
Eureka es el registro que describe la mayor parte del material sobre Spring Cloud. El servidor es una aplicación Boot con spring-cloud-starter-netflix-eureka-server y @EnableEurekaServer; cada servicio añade spring-cloud-starter-netflix-eureka-client y se registra con su spring.application.name:
eureka:
client:
service-url:
defaultZone: http://eureka-1:8761/eureka/,http://eureka-2:8761/eureka/
instance:
prefer-ip-address: true
lease-renewal-interval-in-seconds: 10
Eureka prioriza la disponibilidad sobre la consistencia: las instancias siguen apareciendo un tiempo después de morir, y un servidor que pierde el contacto con sus pares sigue sirviendo el último registro conocido (modo self-preservation). Por eso los clientes necesitan retries y timeouts en cualquier caso: el registro es una caché, no la verdad.
En el lado del cliente está Spring Cloud LoadBalancer, que resuelve un nombre de host lógico a una instancia y aplica una política round-robin (por defecto) o aleatoria. Los servicios con Boot 3.2+ deberían usar un RestClient con balanceo de carga:
@Configuration
class ClientConfig {
@Bean
@LoadBalanced
RestClient.Builder loadBalancedRestClientBuilder() {
return RestClient.builder();
}
@Bean
InventoryApi inventoryApi(@LoadBalanced RestClient.Builder builder) {
RestClient client = builder.baseUrl("http://inventory").build(); // "inventory" is a service name
return HttpServiceProxyFactory.builderFor(RestClientAdapter.create(client))
.build().createClient(InventoryApi.class);
}
}
@LoadBalanced marca el builder para que un interceptor sustituya inventory por un host y un puerto reales en cada llamada. Spring Cloud OpenFeign ofrece lo mismo mediante interfaces @FeignClient(name = "inventory"), con un repertorio más amplio de anotaciones de contrato y encoders; sigue teniendo soporte, pero el código nuevo debería preferir las HTTP interfaces del framework, que no requieren ningún proyecto adicional y funcionan con el mismo balanceador.
En Kubernetes, todo esto es opcional. Un Service llamado inventory ya se resuelve por DNS a una IP virtual con balanceo de carga, así que http://inventory funciona con un RestClient normal y sin registro. Spring Cloud Kubernetes existe para las aplicaciones que quieren que la abstracción DiscoveryClient lea los endpoints de Kubernetes, sobre todo en despliegues híbridos.
El gateway
Los clientes externos no deberían conocer la topología de tus servicios. Un API gateway es el punto de entrada único que enruta las peticiones a los servicios, termina el TLS, aplica autenticación y rate limiting y reescribe rutas. Spring Cloud Gateway está construido sobre Spring WebFlux y Project Reactor (existe una variante basada en servlets, Spring Cloud Gateway Server MVC, para equipos que quieren un stack bloqueante), y las rutas se declaran en la configuración o en Java:
spring:
cloud:
gateway:
routes:
- id: orders
uri: lb://orders # lb:// resolves through the load balancer and registry
predicates:
- Path=/api/orders/**
filters:
- StripPrefix=1 # /api/orders/42 -> /orders/42 on the service
- name: CircuitBreaker
args:
name: ordersBreaker
fallbackUri: forward:/fallback/orders
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 50
redis-rate-limiter.burstCapacity: 100
- id: catalogue
uri: http://catalogue:8080
predicates:
- Path=/api/catalogue/**
- Method=GET
filters:
- AddResponseHeader=Cache-Control, public, max-age=60
Un predicate decide si una ruta coincide (path, método, cabecera, host, query, hora del día); un filter transforma la petición o la respuesta (quitar o reescribir paths, añadir cabeceras, retry, rate limiting, circuit breaking, reenviar un token OAuth 2.0). Los global filters se aplican a todas las rutas y suelen ser el sitio de los correlation ids, el logging de peticiones y la autenticación. Con el soporte de resource server OAuth 2.0 de Spring Security, el gateway valida los access tokens una sola vez y los reenvía, de modo que los servicios downstream pueden confiar en la cabecera Authorization; el filter TokenRelay se encarga del reenvío.
En el release train 2025.0 se renombraron los artefactos del gateway para dejar explícitos los dos stacks (spring-cloud-starter-gateway-server-webflux y spring-cloud-starter-gateway-server-webmvc, con prefijos de propiedades bajo spring.cloud.gateway.server.*); los nombres antiguos siguen disponibles durante un periodo de transición. Consulta la documentación de referencia del train que uses antes de copiar nombres de propiedades.
Resiliencia: circuit breakers, retries y límites de tiempo
Una dependencia lenta es más peligrosa que una caída: los threads se acumulan esperando y quien llama se vuelve lento para todo el mundo. Resilience4j aporta los patrones y Spring Cloud Circuit Breaker los envuelve tras una factory independiente del proveedor:
@Service
class RecommendationService {
private final CircuitBreakerFactory<?, ?> breakers;
private final RecommendationApi api;
RecommendationService(CircuitBreakerFactory<?, ?> breakers, RecommendationApi api) {
this.breakers = breakers;
this.api = api;
}
List<Product> forCustomer(long customerId) {
return breakers.create("recommendations")
.run(() -> api.recommend(customerId),
throwable -> List.of()); // fallback: an empty list, not an error page
}
}
resilience4j:
circuitbreaker:
instances:
recommendations:
sliding-window-size: 20
failure-rate-threshold: 50 # open after 50 % failures in the window
wait-duration-in-open-state: 30s
permitted-number-of-calls-in-half-open-state: 5
timelimiter:
instances:
recommendations:
timeout-duration: 800ms
Un circuit breaker cuenta los fallos en una ventana deslizante; al superar el umbral se abre y hace fallar las llamadas de inmediato sin tocar la dependencia; tras una espera deja pasar unas pocas llamadas de prueba (half-open) y vuelve a cerrarse si tienen éxito. Un time limiter acota cuánto puede durar una llamada. Un retry repite una llamada fallida con backoff, lo que ayuda con errores transitorios y empeora las cosas ante una sobrecarga, así que combínalo con el circuit breaker y mantén bajo el número de intentos. Un bulkhead limita las llamadas concurrentes a una dependencia para que una lenta no acapare todos los threads. El starter resilience4j-spring-boot3 ofrece también anotaciones (@CircuitBreaker(name = "recommendations", fallbackMethod = "empty")) que se aplican mediante AOP, y publica estado y métricas en Actuator y Micrometer.
Los fallbacks deben tener sentido: una lista de recomendaciones vacía, un precio en caché, una respuesta de «inténtalo más tarde» con la cabecera Retry-After. Un fallback que devuelve en silencio datos obsoletos o inventados es un bug con efecto retardado.
Cómo encaja todo
Una topología mínima pero realista sobre máquinas virtuales: dos instancias de Config Server respaldadas por un mismo repositorio Git; dos instancias de Eureka que se registran entre sí; un gateway que enruta /api/** a los servicios por su nombre, con un circuit breaker por ruta; cada servicio obtiene su configuración al arrancar, se registra en Eureka, llama a sus pares mediante HTTP interfaces con balanceo de carga protegidas por Resilience4j y reporta health, métricas y trazas a través de Actuator. En Kubernetes, elimina Eureka y quizá el Config Server (ConfigMaps y Secrets con spring.config.import=kubernetes: o config trees montados), mantén el gateway o sustitúyelo por un ingress más seguridad en cada servicio, y conserva la capa de resiliencia, que ninguna plataforma te da hecha.
Errores comunes
Mezclar release trains y versiones de Boot. El BOM debe corresponder a la versión menor de Boot; los síntomas van desde un NoSuchMethodError al arrancar hasta beans que faltan sin avisar.
Copiar ejemplos de Hystrix, Ribbon o Zuul. No compilarán con los trains actuales. Tradúcelos a Resilience4j, Spring Cloud LoadBalancer y Spring Cloud Gateway.
Tratar el registro como fuente de verdad. Las instancias que se han caído siguen listadas hasta que caduca su lease. Todo cliente necesita timeouts y retries con backoff, con independencia del discovery.
Refrescar con @RefreshScope beans que guardan estado o recursos. El proxy recrea el bean y se lleva por delante cachés, pools de conexiones y tareas programadas. Vuelve a enlazar @ConfigurationProperties en su lugar y reserva @RefreshScope para beans baratos de reconstruir.
Una sola instancia de Config Server o de registro. Ambos están en el camino crítico del arranque; ejecuta al menos dos y configura fail-fast de forma deliberada según si un servicio puede arrancar con valores locales por defecto desactualizados.
Reintentar llamadas no idempotentes. Un POST /payments reintentado cobra dos veces. Reintenta solo operaciones idempotentes, o haz la operación idempotente con una clave generada por el cliente.
Poner lógica de negocio en los filters del gateway. El gateway debe enrutar, proteger, limitar y observar. La transformación de payloads y la orquestación corresponden a los servicios, donde pueden probarse y versionarse junto con el dominio.
Puntos clave
- Spring Cloud es un conjunto de proyectos coordinados que se publican como trains; importa el BOM que corresponde a la versión menor de tu Spring Boot y nunca los mezcles.
- Config Server sirve configuración versionada desde Git por aplicación, perfil y label; los clientes la importan con
spring.config.importy la refrescan a través de Actuator o Spring Cloud Bus. - El discovery (Eureka, Consul, Kubernetes) junto con Spring Cloud LoadBalancer permite que los servicios se llamen por nombre mediante un
RestClient@LoadBalancedo una HTTP interface; en Kubernetes, el DNS suele hacer innecesario el registro. - Spring Cloud Gateway enruta el tráfico externo con predicates y filters, y es donde deben vivir la autenticación, el rate limiting y los circuit breakers por ruta.
- Los circuit breakers, time limiters, retries y bulkheads de Resilience4j evitan que una sola dependencia lenta tumbe a quien la llama; los fallbacks deben devolver algo que el negocio pueda aceptar.
- Hystrix, Ribbon y Zuul han desaparecido; Sleuth se convirtió en Micrometer Tracing; OpenFeign está en modo mantenimiento en favor de las HTTP interfaces de Spring.
Fuentes y lecturas recomendadas
Preguntas frecuentes
- ¿Qué versión de Spring Cloud funciona con mi versión de Spring Boot?
- Spring Cloud se publica en release trains que llevaban nombres de estaciones del metro de Londres y, desde 2020, se nombran por año. El train 2023.0.x (Leyton) está pensado para Spring Boot 3.2 y 3.3; 2024.0.x (Moorgate), para Boot 3.4; 2025.0.x (Northfields), para Boot 3.5. Importa el BOM spring-cloud-dependencies del train correspondiente y deja que gestione la versión de cada spring-cloud-starter; la tabla de compatibilidad de la página del proyecto Spring Cloud es la referencia.
- ¿Sigo necesitando Eureka en Kubernetes?
- Normalmente no. Los Services de Kubernetes ya ofrecen discovery basado en DNS y balanceo de carga, y un Spring Cloud Gateway o un ingress controller puede enrutar por nombre de servicio. Eureka sigue teniendo sentido en máquinas virtuales, en plataformas sin una abstracción de servicio o cuando necesitas decisiones de balanceo en el cliente que Kubernetes no ofrece. Spring Cloud Kubernetes tiende un puente entre ambos cuando una misma base de código debe ejecutarse en los dos.
- ¿Está obsoleto Spring Cloud OpenFeign?
- Está en modo mantenimiento: recibe correcciones de errores y sigue siendo compatible con los nuevos release trains, pero el equipo de Spring recomienda para código nuevo los clientes de HTTP interface integrados en Spring Framework 6. Los clientes Feign existentes siguen funcionando; la migración puede hacerse poco a poco, interfaz por interfaz, porque en ambos casos se trata de interfaces declarativas respaldadas por un cliente con balanceo de carga.
- ¿Qué pasó con Hystrix, Ribbon y Zuul?
- Netflix dejó de desarrollarlos y Spring Cloud eliminó sus integraciones en el release train 2020.0 (Ilford). Sus reemplazos son Spring Cloud Circuit Breaker con Resilience4j para Hystrix, Spring Cloud LoadBalancer para Ribbon y Spring Cloud Gateway para Zuul. Eureka es el único componente de Netflix que queda en Spring Cloud Netflix.