Skip to content

Circuit Breaker (Resilience4j)

Guía definitiva de Circuit Breakers en Spring Boot con Resilience4j

📖 ¿Quieres ver este documento más hermoso? Haz clic aquí para la versión HTML con diseño oscuro/claro, tabla de contenidos lateral y más.

1. Introducción a la Resiliencia en Sistemas Distribuidos

Section titled “1. Introducción a la Resiliencia en Sistemas Distribuidos”

En arquitecturas de microservicios las llamadas remotas caen por debajo de tu control y un fallo parcial puede propagarse en cascada por todo el clúster.

  • [Usuario] ──> [Servicio Edge] ──> [Servicio Pedidos] ──> [Pasarela de Pago LENTA/CAÍDA]
  • │ │
  • └─(Hilos Bloqueados)───└─(Hilos Agotados esperando timeout)

Fallos en Cascada y Agotamiento del Pool de Hilos

Section titled “Fallos en Cascada y Agotamiento del Pool de Hilos”

Cuando un servicio aguas abajo es lento, los hilos del cliente quedan bloqueados. Bajo carga eso deriva rápidamente en Thread Pool Exhaustion. El servicio cliente deja de responder incluso para tráfico que no depende del sistema roto.

Para construir sistemas resilientes hay que diseñar para el fallo y fallar rápido (Fail Fast).

La diferencia no está en si esa transacción específica se completa, sino en qué le pasa a todo el sistema mientras persiste la falla.

  • Escenario A: SIN Circuit Breaker: un servicio lento inmoviliza hilos por timeout. Bajo carga, el pool se agota y el sistema entero deja de responder.
  • Escenario B: CON Circuit Breaker: los primeros fallos abren el disyuntor a OPEN. La llamada se rechaza en milisegundos, se ejecuta un fallback y el resto del sistema sigue operando.

El patrón Circuit Breaker envuelve llamadas remotas y monitoriza fallos. Funciona como un fusible eléctrico: corta la conexión con un servicio en fallo para proteger al sistema que consume ese servicio.

  • CLOSED: las llamadas siguen fluyendo. Si se supera el umbral, pasa a OPEN.
  • OPEN: las llamadas se rechazan de inmediato y se ejecuta un fallback.
  • HALF-OPEN: se permiten pocas llamadas de prueba; si fallan regresa a OPEN, si tienen éxito vuelve a CLOSED.
  • CLOSED: inventario responde en 50 ms y la compra se procesa con normalidad.
  • OPEN: el inventario falla. El disyuntor salta a OPEN y ejecuta un fallback: guardar el pedido y posponer el cobro.
  • HALF-OPEN: pasado el tiempo de espera, se prueban pocas compras. Si todas tienen éxito, el circuito vuelve a CLOSED.

App de Transporte: Cotización -> Google Maps

Section titled “App de Transporte: Cotización -> Google Maps”
  • CLOSED: consulta exitosa en 100 ms. Tarifa y ruta presentadas al instante.
  • OPEN: la API devuelve error 403. El disyuntor abre y el fallback calcula distancia en línea recta.

Streaming: Home -> Motor de Recomendaciones

Section titled “Streaming: Home -> Motor de Recomendaciones”
  • CLOSED: respuesta en 150 ms con filas personalizadas.
  • OPEN: el motor se satura. El disyuntor abre y sirve una caché de contenido popular.

3. Evolución de los Circuit Breakers en Spring: de Hystrix a Resilience4j

Section titled “3. Evolución de los Circuit Breakers en Spring: de Hystrix a Resilience4j”

Netflix Hystrix fue referencia durante años, pero quedó deprecado en favor de soluciones más ligeras como Resilience4j.

¿Por qué se deprecó Hystrix?

  • Diseño monolítico y acoplamiento fuerte a RxJava.
  • Introducía overhead de CPU por pools de hilos dedicados.
  • Sin soporte oficial para Spring Boot 3.
CaracterísticaNetflix HystrixResilience4j
ParadigmaReactivoFuncional / Lambdas (Java 8+)
DependenciasPesadasLigeras y modulares
Spring Boot 3No soportadoSoporte de primer nivel
MemoriaModerada / AltaExtremadamente baja

Resilience4j evalúa el comportamiento reciente mediante una ventana deslizante, que puede ser:

  • COUNT_BASED: solo considera las últimas N llamadas.
  • TIME_BASED: considera solo las llamadas dentro de los últimos N segundos.

Si la ventana no es deslizante, éxitos antiguos diluyen fallos recientes. La ventana deslizante decide usando únicamente el presente inmediato.

  • Cinta transportadora (Count-Based): buffer circular con las últimas llamadas. Una nueva llamada reemplaza a la más antigua.
  • Cubos de tiempo (Time-Based): buckets parciales dentro de la ventana temporal. El bucket más viejo se descarta conforme avanza el tiempo.

Configuremos una aplicación Spring Boot moderna con Resilience4j.

<!-- Resilience4j Spring Boot Starter -->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
<version>2.2.0</version>
</dependency>
<!-- Spring Boot Starter AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
resilience4j:
circuitbreaker:
configs:
default:
slidingWindowSize: 100
slidingWindowType: COUNT_BASED
minimumNumberOfCalls: 10
failureRateThreshold: 50
slowCallRateThreshold: 75
slowCallDurationThreshold: 2000ms
waitDurationInOpenState: 15000ms
permittedNumberOfCallsInHalfOpenState: 5
automaticTransitionFromOpenToHalfOpenEnabled: true
instances:
paymentServiceBreaker:
baseConfig: default
slidingWindowSize: 20
failureRateThreshold: 40

Resilience4j intercepta métodos anotados con @CircuitBreaker mediante Spring AOP.

@Service
public class PaymentService {
private final RestTemplate restTemplate;
@CircuitBreaker(name = "paymentServiceBreaker", fallbackMethod = "processPaymentFallback")
public String processPayment(String transactionId, double amount) {
String externalUrl = "https://api.external-payment.com/v1/charge";
return restTemplate.postForObject(externalUrl, new PaymentRequest(transactionId, amount), String.class);
}
public String processPaymentFallback(String transactionId, double amount, Throwable exception) {
if (exception instanceof CallNotPermittedException) {
return "PAYMENT_SERVICE_UNAVAILABLE (Fallo Rápido)";
}
return "PAYMENT_PENDING (Almacenado localmente para procesamiento diferido)";
}
}

Reglas de firmas fallback: el método debe estar en la misma clase, aceptar los mismos parámetros más Throwable final, y devolver el mismo tipo.

No todas las excepciones representan fallos de infraestructura. Por ejemplo, las de negocio 4xx.

resilience4j.circuitbreaker:
instances:
orderServiceBreaker:
recordExceptions:
- org.springframework.web.client.HttpServerErrorException
- java.util.concurrent.TimeoutException
ignoreExceptions:
- org.springframework.web.client.HttpClientErrorException

Eventos de transición de estado:

@Configuration
public class CircuitBreakerEventListener {
@PostConstruct
public void registerListeners() {
registry.getAllCircuitBreakers().forEach(breaker -> {
breaker.getEventPublisher()
.onStateTransition(event -> log.error("Transición: {}", event.getStateTransition()));
});
}
}

La envoltura programática evita los límites del proxy de Spring AOP.

@Service
public class ProgrammaticPaymentService {
private final CircuitBreaker circuitBreaker;
public String processPayment(String txId, double amount) {
Supplier<String> paymentCall = () -> executePayment(txId, amount);
Supplier<String> decorated = CircuitBreaker.decorateSupplier(circuitBreaker, paymentCall);
return Try.ofSupplier(decorated)
.recover(throwable -> "FALLBACK_TRIGGERED")
.get();
}
}

Spring Boot Actuator y Micrometer exponen métricas del estado de los circuit breakers.

  • resilience4j_circuitbreaker_state: estado actual.
  • resilience4j_circuitbreaker_calls: llamadas por resultado.
  • resilience4j_circuitbreaker_failure_rate: tasa de fallos.

Verifica transiciones de estado con pruebas de integración robustas.

@SpringBootTest
class PaymentServiceIntegrationTest {
@Autowired private PaymentService paymentService;
@Test
void testCircuitBreakerTrips() {
when(restTemplate.postForObject(any(), any(), any()))
.thenThrow(new RuntimeException("Network Error"));
for (int i = 0; i < 11; i++) {
paymentService.processPayment("tx", 10.0);
}
assertEquals(CircuitBreaker.State.OPEN, circuitBreaker.getState());
}
}
  • Evita autollamadas: invocar un método @CircuitBreaker desde la misma clase elude el proxy AOP.
  • Ajusta timeouts HTTP: mantén timeouts agresivos para que el circuito no quede retenido por llamadas lentas.