El problema: ¿por qué necesitamos resiliencia?

En los sistemas distribuidos, las aplicaciones dependen de múltiples servicios externos (APIs, bases de datos, etc.). Un fallo en uno de estos servicios puede causar un fallo en cascada, donde la lentitud o caída de un componente bloquea recursos y tumba toda la aplicación.

Problemas comunes

El objetivo de Resilience4j es dotar a la aplicación de la capacidad de resistir, adaptarse y recuperarse de estos fallos sin colapsar, mejorando la experiencia del usuario y la estabilidad del sistema.

Evolución de Resilience4j dentro de Spring Boot

Resilience4j ha evolucionado significativamente desde su creación en 2015. La biblioteca pasó de ser un proyecto experimental a convertirse en el estándar de facto para tolerancia a fallos en aplicaciones Spring Boot. El hito más importante fue la versión 2.0.0 (2022), que estableció Java 17 como requisito mínimo y crear módulos separados para Spring Boot 2 (resilience4j-spring-boot2) y Spring Boot 3 (resilience4j-spring-boot3).

Actualmente, en su versión 2.3.0 (2025), Resilience4j ofrece integración nativa con Spring Boot 3.2+, soporte completo para programación reactiva, observabilidad mejorada con Actuator y Micrometer y se posiciona como una solución madura y estable para implementar patrones de resiliencia (Circuit Breaker, Retry, Rate Limiter, Bulkhead, Time Limiter) en arquitecturas de microservicios modernas.

Conceptos fundamentales de Resilience4j

Resilience4j implementa varios patrones de resiliencia. Hay que tener en cuenta que esta librería y sus estrategias se pueden aplicar a sistemas que tengan dependencias externas independientemente de su diseño (monolíticos, micros etc..).

Pero, dada la idiosincrasia de los microservicios, su uso y estrategias encajan perfectamente. Los más importantes son:

Circuit Breaker (interruptor de circuito)

Estados:

Configuración clave:

Properties:

# Circuit Breaker - Configuración por defecto
resilience4j.circuitbreaker.configs.default.slidingWindowSize=100
resilience4j.circuitbreaker.configs.default.minimumNumberOfCalls=10
resilience4j.circuitbreaker.configs.default.failureRateThreshold=50
resilience4j.circuitbreaker.configs.default.waitDurationInOpenState=60000
resilience4j.circuitbreaker.configs.default.permittedNumberOfCallsInHalfOpenState=10
resilience4j.circuitbreaker.configs.default.automaticTransitionFromOpenToHalfOpenEnabled=true
resilience4j.circuitbreaker.configs.default.registerHealthIndicator=true

# Circuit Breaker - Servicio de Emails (SendGrid)
resilience4j.circuitbreaker.instances.emailService.baseConfig=default
resilience4j.circuitbreaker.instances.emailService.failureRateThreshold=60
resilience4j.circuitbreaker.instances.emailService.waitDurationInOpenState=120000

Retry (reintento)

Busca reintentar una operación que ha fallado por un problema temporal (ej. un timeout de red).

Backoff exponencial
Backoff exponencial
Jitter (variación)
Jitter (variación)

Configuración:

# Retry - Configuración por defecto
resilience4j.retry.configs.default.maxAttempts=3
resilience4j.retry.configs.default.waitDuration=1000
resilience4j.retry.configs.default.exponentialBackoffMultiplier=2
resilience4j.retry.configs.default.retryExceptions=java.net.SocketTimeoutException,java.net.ConnectException

# Retry - Servicio de Emails
resilience4j.retry.instances.emailService.baseConfig=default
resilience4j.retry.instances.emailService.maxAttempts=2
resilience4j.retry.instances.emailService.waitDuration=2000

Nunca uses reintentos automáticos en operaciones que no sean idempotentes, como procesar un pago, para evitar duplicados.

Rate Limiter (limitador de tasa)

Protege tu API de un número excesivo de peticiones, ya sea por abuso o sobrecarga (ej. Black Friday). Limita el número de peticiones en un periodo de tiempo.

Configuración clave:

resilience4j.ratelimiter:
  instances:
    publicApi:
      limitForPeriod: 100      # 100 peticiones
      limitRefreshPeriod: 1s   # por segundo
      timeoutDuration: 0       # Rechazar inmediatamente si se excede

Bulkhead (compartimento estanco)

Aísla los recursos (hilos) para que un servicio lento no acapare todos los recursos de la aplicación. Limita el número de llamadas concurrentes a un servicio específico. Si un servicio se satura, solo afectará a su "compartimento", no al resto de la aplicación.

Configuración clave:

resilience4j.bulkhead:
  instances:
    emailService:
      maxConcurrentCalls: 30 # Máximo 30 llamadas simultáneas

Time Limiter (limitador de tiempo)

Establece un timeout máximo para una operación. Si la operación tarda más de lo definido, se cancela y lanza una TimeoutException. Es fundamental para evitar hilos bloqueados indefinidamente.

Configuración clave:

# Time Limiter - Configuración por defecto
resilience4j.timelimiter.configs.default.timeoutDuration=3000

# Time Limiter - Servicio de Emails
resilience4j.timelimiter.instances.emailService.timeoutDuration=15000

Implementación práctica en Spring Boot

Dependencias Maven

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot3</artifactId>
    <version>2.1.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Ejemplo combinado: proteger un servicio crítico

El verdadero poder de Resilience4j reside en combinar patrones. Este es un ejemplo para proteger una llamada a un servicio de pagos.

Lo primero es establecer la configuración por defecto para que quede claro que está activa. Esto aplica a todos los patrones que vamos a explicar a continuación:

resilience4j.auto-configuration.enabled=true
# Circuit Breaker - Servicio de Pagos
resilience4j.circuitbreaker.instances.paymentService.baseConfig=default
resilience4j.circuitbreaker.instances.paymentService.failureRateThreshold=40
resilience4j.circuitbreaker.instances.paymentService.waitDurationInOpenState=180000

# Time Limiter - Servicio de Pagos (10 segundos para dar margen)
resilience4j.timelimiter.instances.paymentService.timeoutDuration=10000

# IMPORTANTE: NO configurar Retry para pagos
# Los pagos NO deben reintentar automáticamente para evitar cobros duplicados

Código Java:

@Service
public class RedsysPaymentServiceImpl {
   private static final Logger logger = LoggerFactory.getLogger(RedsysPaymentServiceImpl.class);
   private final PaymentRepository paymentRepository;
   private final OrderRepository orderRepository;
   private final OrderMailPaymentService orderMailPaymentService;


   /**
    * Procesa el pago Redsys con protección completa de Resilience4j.
    *
    * Resilience4j:
    * - Circuit Breaker: Protege contra fallos del servicio de pagos
    * - Retry: NO se usa aquí (pagos no deben reintentar automáticamente)
    * - Time Limiter: Cancela si tarda más de 10 segundos
    * - Fallback: Marca pedido como pendiente de revisión manual
    */
   @CircuitBreaker(name = "paymentService", fallbackMethod = "fallbackProcessRedsysPayment")
   @TimeLimiter(name = "paymentService")
   public CompletableFuture<RedsysPaymentResponseDTO> processRedsysPayment(RedsysPaymentRequestDTO request) {
       return CompletableFuture.supplyAsync(() -> {
           logger.info("[RedsysService] Procesando pago para orderId: {}", request.getOrderId());


           // Simulación de retardo para Redsys (3 segundos)
           try {
               Thread.sleep(3000);
           } catch (InterruptedException e) {
               Thread.currentThread().interrupt();
               throw new RuntimeException("Pago interrumpido", e);
           }


           // Crear y guardar el pago
           Payment payment = Payment.builder()
                   .orderId(request.getOrderId())
                   .amount(request.getAmount())
                   .method(PaymentMethod.REDSYS)
                   .status(PaymentStatus.COMPLETED)
                   .transactionId("REDSYS-" + System.currentTimeMillis())
                   .build();


           Payment saved = paymentRepository.save(payment);


           // Actualizar estado del pedido a COMPLETED
           orderRepository.findByOrderId(request.getOrderId()).ifPresent(order -> {
               order.setStatus("COMPLETED");
               orderRepository.save(order);
               logger.info("[RedsysService] Order actualizado a COMPLETED: {}", order.getOrderId());


               // Enviar email de confirmación (protegido con su propio Circuit Breaker)
               orderMailPaymentService.sendOrderConfirmationEmail(order.getOrderId());
           });


           return paymentMapper.toRedsysPaymentResponseDTO(saved);
       });
   }


   /**
    * Fallback cuando el servicio de pagos falla.
    * IMPORTANTE: NO reintentar pagos automáticamente para evitar cobros duplicados.
    */
   private CompletableFuture<RedsysPaymentResponseDTO> fallbackProcessRedsysPayment(
           RedsysPaymentRequestDTO request, Exception e) {
       logger.error("⚠️ FALLBACK: Servicio de pago Redsys no disponible para orderId={}. Causa: {}",
                    request.getOrderId(), e.getMessage(), e);


       // Marcar el pedido como pendiente de revisión manual
       orderRepository.findByOrderId(request.getOrderId()).ifPresent(order -> {
           order.setStatus("PENDING_PAYMENT");
           orderRepository.save(order);
           logger.warn("[RedsysService] Order marcado como PENDING_PAYMENT para revisión manual: {}",
                       order.getOrderId());
       });


       // Crear registro de pago fallido para auditoría
       Payment failedPayment = Payment.builder()
               .orderId(request.getOrderId())
               .amount(request.getAmount())
               .method(PaymentMethod.REDSYS)
               .status(PaymentStatus.FAILED)
               .providerResponse("Servicio temporalmente no disponible: " + e.getMessage())
               .build();
       paymentRepository.save(failedPayment);


       // Devolver respuesta indicando que el pago está pendiente
       RedsysPaymentResponseDTO fallbackResponse = new RedsysPaymentResponseDTO();
       fallbackResponse.setStatus(PaymentStatus.PENDING);
       fallbackResponse.setMessage("El servicio de pago no está disponible. " +
                                  "Tu pedido ha sido guardado y será procesado manualmente. " +
                                  "Recibirás un email de confirmación en breve.");


       return CompletableFuture.completedFuture(fallbackResponse);
   }
}

Beneficios de esta combinación

Los beneficios son, principalmente, dos:

Guía de decisión: ¿qué patrón usar?

Si tu servicio... Patrón recomendado Ejemplo de uso
Puede caerse por completo o ser muy inestable Circuit Breaker + Fallback (obligatorio) API de pagos, servicio de emails, base de datos externa.
Sufre fallos temporales (ej. red) Retry (solo para operaciones idempotentes) Lecturas de una base de datos, llamada a una API de consulta.
Necesita protección contra sobrecarga o abuso Rate Limiter Endpoints públicos de tu API, endpoints de login.
Consume muchos recursos y puede afectar a otros Bulkhead Operaciones pesadas, queries complejas a la BD.
A veces tarda demasiado en responder Time Limiter Llamadas a APIs de terceros sin un SLA claro.

Mejores prácticas esenciales

  1. Usa Fallbacks útiles. Un fallback no debe simplemente lanzar otra excepción. Debe ofrecer una alternativa real: devolver datos de caché, encolar una tarea para más tarde o devolver un valor por defecto.
  2. No abuses de retry. Úsalo solo para fallos transitorios y en operaciones idempotentes (que se pueden repetir sin causar efectos secundarios). Nunca en un envío de formulario de pago sin una clave de idempotencia.
  3. Configura excepciones específicas. Configura retryExceptions e ignoreExceptions para reintentar solo los fallos que tienen sentido (ej. SocketTimeoutException) y no los que son definitivos (ej. InsufficientFundsException).
  4. Establece timeouts realistas. Un timeout demasiado corto causará fallos innecesarios. Basa la configuración en la latencia observada del servicio (ej. percentil 99).
  5. Monitoriza tus patrones. Resilience4j se integra con Spring Boot Actuator (/actuator/circuitbreakers, /actuator/retries). Usa estas métricas para visualizar el estado de tus servicios en herramientas como Prometheus y Grafana y ajusta la configuración.

Conclusión

Integrar Resilience4j no es una opción, sino una necesidad en arquitecturas de microservicios. Te permite pasar de un sistema frágil que falla por completo a uno robusto y resiliente que puede soportar fallos de sus dependencias de manera elegante.

Recuerda: "no es cuestión de SI un servicio externo fallará, sino de CUÁNDO. Prepárate."

Con patrones como Circuit Breaker, Retry y Fallback puedes construir aplicaciones que no solo sobreviven a los fallos, sino que se recuperan automáticamente, garantizando una mejor experiencia para el usuario y una mayor tranquilidad para el equipo de desarrollo

Adicionalmente, he generado en mi proyecto infinia-sports unos test de carga con Resilience4j con gatling y he generado unos informes con las tasas de mejora del sistema que son muy significativos.

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete