En pocas palabras
Librería de tolerancia a fallos para Java inspirada en Hystrix, diseñada con programación funcional, módulos independientes e integración con Spring Boot y Micrometer.
Qué problema resuelve
Implementar a mano reintentos, timeouts y circuit breakers de forma inconsistente en cada integración.
Cuándo utilizarlo
Servicios JVM que llaman a APIs externas o internas. Ejemplo: proteger la consulta al buró de crédito con timeout, un reintento y circuit breaker.
De la idea a la práctica
Funcionamiento, ejemplos y decisiones para pasar al siguiente nivel.
Módulos
| Módulo | Función |
|---|---|
CircuitBreaker |
Abre el circuito según tasa de fallos o de llamadas lentas |
Retry |
Reintentos con backoff configurable y filtros por excepción |
TimeLimiter |
Límite de tiempo para llamadas asíncronas (CompletableFuture) |
Bulkhead |
Limita llamadas concurrentes (semáforo o pool de hilos) |
RateLimiter |
Limita llamadas por periodo |
Se combinan decorando la misma función. Con anotaciones en Spring, el orden de aplicación por defecto es Retry(CircuitBreaker(RateLimiter(TimeLimiter(Bulkhead(función))))).
Ejemplo con Spring Boot
@Service
class ClienteBuro {
@CircuitBreaker(name = "buro", fallbackMethod = "pendiente")
@Retry(name = "buro")
@Bulkhead(name = "buro")
public ResultadoBuro consultar(String documento) {
return restClient.get().uri("/historial/{doc}", documento)
.retrieve().body(ResultadoBuro.class);
}
private ResultadoBuro pendiente(String documento, Throwable causa) {
return ResultadoBuro.pendiente(documento); // la solicitud queda PENDIENTE_BURO
}
}
resilience4j:
circuitbreaker:
instances:
buro:
sliding-window-type: COUNT_BASED
sliding-window-size: 20
minimum-number-of-calls: 10
failure-rate-threshold: 50
slow-call-duration-threshold: 2s
slow-call-rate-threshold: 50
wait-duration-in-open-state: 30s
permitted-number-of-calls-in-half-open-state: 3
retry:
instances:
buro:
max-attempts: 2
wait-duration: 200ms
enable-exponential-backoff: true
enable-randomized-wait: true
retry-exceptions:
- java.net.SocketTimeoutException
- org.springframework.web.client.HttpServerErrorException
bulkhead:
instances:
buro:
max-concurrent-calls: 20
Requiere spring-boot-starter-aop (o el starter de AspectJ según la versión) para que funcionen las anotaciones. El timeout de la llamada síncrona se configura en el propio cliente HTTP; TimeLimiter aplica solo a llamadas asíncronas.
Observabilidad
Con Actuator y Micrometer se exponen métricas como resilience4j_circuitbreaker_state, resilience4j_circuitbreaker_calls y resilience4j_retry_calls, además del endpoint /actuator/circuitbreakers. Alerta cuando un circuito permanezca abierto y grafica la tasa de fallbacks: es una degradación visible para el negocio.
Antipatrones frecuentes
- Reintentar excepciones de negocio (
400, validación): nunca van a tener éxito. max-attemptsalto en varias capas, que multiplica la carga sobre la dependencia caída.- Fallbacks que ocultan errores sin métricas: el sistema «funciona» mientras pierde ventas.
Uso programático sin anotaciones
CircuitBreaker cb = circuitBreakerRegistry.circuitBreaker("buro");
Retry retry = retryRegistry.retry("buro");
Bulkhead bulkhead = bulkheadRegistry.bulkhead("buro");
Supplier<ResultadoBuro> llamada = Decorators.ofSupplier(() -> cliente.consultar(documento))
.withBulkhead(bulkhead)
.withCircuitBreaker(cb)
.withRetry(retry)
.withFallback(List.of(CallNotPermittedException.class, BulkheadFullException.class),
e -> ResultadoBuro.pendiente(documento))
.decorate();
ResultadoBuro resultado = llamada.get();
El uso programático hace explícito el orden de los decoradores y funciona fuera de beans de Spring (por ejemplo, en librerías o en código sin AOP).
Timeouts del cliente HTTP
@Bean
RestClient clienteBuro(BuroProperties props) {
var factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(Duration.ofSeconds(1));
factory.setReadTimeout(props.timeout()); // 3 s
return RestClient.builder()
.baseUrl(props.baseUrl().toString())
.requestFactory(factory)
.build();
}
Sin timeouts en el cliente, CircuitBreaker solo se entera del fallo cuando la llamada termina, que puede ser nunca.
Rate limiter para cuotas de terceros
resilience4j:
ratelimiter:
instances:
buro:
limit-for-period: 50 # el contrato permite 50 consultas por segundo
limit-refresh-period: 1s
timeout-duration: 200ms # espera máxima por un permiso antes de fallar
Protege al proveedor y evita penalizaciones por exceder la cuota contratada. Si hay varias réplicas, el límite es por instancia: divide la cuota entre réplicas o usa un limitador distribuido en el gateway.
Probar el comportamiento
@Test
void abreElCircuitoTrasFallosYUsaFallback() {
wireMock.stubFor(get(urlPathMatching("/historial/.*")).willReturn(serverError()));
IntStream.range(0, 20).forEach(i -> servicio.consultar("123"));
assertThat(circuitBreakerRegistry.circuitBreaker("buro").getState()).isEqualTo(CircuitBreaker.State.OPEN);
assertThat(servicio.consultar("123").estado()).isEqualTo(Estado.PENDIENTE_BURO);
}
Usa en las pruebas una configuración con ventanas y esperas cortas para que los cambios de estado ocurran en milisegundos.
Alternativas
Spring Cloud Circuit Breaker ofrece una abstracción sobre Resilience4j; en Kubernetes, un service mesh puede aplicar timeouts, reintentos y outlier detection fuera del código; Dapr define políticas declarativas. La lógica de fallback de negocio, en cualquier caso, sigue en la aplicación.
Criterios de implementación
Configura una instancia por dependencia, nunca una global. Deriva los valores de la latencia y la tasa de error observadas, siguiendo los patrones de resiliencia. Prueba el comportamiento con inyección de fallos (WireMock con retardos o errores) en las pruebas de integración de Spring Boot.
Continúa en las fuentes
Documentación oficial para verificar detalles y explorar más.