Circuit Breaker básico con Resilience4j
El patrón Circuit Breaker es fundamental en arquitecturas de microservicios para prevenir fallos en cascada. Cuando un servicio remoto falla o se vuelve lento, el circuit breaker actúa como un interruptor automático que corta temporalmente las llamadas, permitiendo que el sistema se recupere sin propagar el error.
Spring Cloud Circuit Breaker proporciona una abstracción que permite utilizar diferentes implementaciones como Resilience4j, Hystrix (deprecado) o Spring Retry. En nuestro caso, utilizaremos Resilience4j que es la implementación recomendada actualmente.
Estados del Circuit Breaker
Un circuit breaker opera en tres estados principales:
- Cerrado (Closed): Estado normal donde las llamadas se ejecutan normalmente
- Abierto (Open): Las llamadas fallan inmediatamente sin ejecutarse cuando se detectan muchos errores
- Semiabierto (Half-Open): Permite algunas llamadas de prueba para verificar si el servicio se ha recuperado
Configuración inicial
Para integrar Resilience4j en nuestro proyecto, añadimos la dependencia de Spring Cloud Circuit Breaker:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
Esta dependencia incluye tanto la integración con Spring Cloud como la librería Resilience4j necesaria.
Configuración básica en application.yml
Configuramos el comportamiento del circuit breaker en nuestro archivo de propiedades, el que está en el repositorio git de configuración centralizada.
Por ejemplo, si tenemos un microservicio llamado customers, que tiene que comunicarse con un microservicio llamado directions, este podría ser el archivo de configuración:
server.port: 7001
server.error.include-message: always
app.message: hello from customers
eureka:
client:
serviceUrl:
defaultZone: "http://${app.eureka-server}:8761/eureka/"
initialInstanceInfoReplicationIntervalSeconds: 5
registryFetchIntervalSeconds: 5
instance:
leaseRenewalIntervalInSeconds: 5
leaseExpirationDurationInSeconds: 5
resilience4j.circuitbreaker:
instances:
directionsCB:
registerHealthIndicator: true
slidingWindowSize: 60
slidingWindowType: TIME_BASED
permittedNumberOfCallsInHalfOpenState: 3
minimumNumberOfCalls: 10
waitDurationInOpenState: 5s
slowCallRateThreshold: 100
slowCallDurationThreshold: 2000
failureRateThreshold: 50
ignoreExceptions:
- com.example.BusinessException
resilience4j.timelimiter:
instances:
directionsTimed:
cancelRunningFuture: false
timeoutDuration: 3s
Estas propiedades definen:
- failure-rate-threshold: Porcentaje de fallos que abre el circuit breaker (50%)
- minimum-number-of-calls: Número mínimo de llamadas antes de evaluar el estado
- wait-duration-in-open-state: Tiempo que permanece abierto antes de pasar a semi-abierto
- sliding-window-size: Tamaño de la ventana deslizante para calcular la tasa de error
Implementación con anotaciones
La forma más sencilla de aplicar el circuit breaker es mediante la anotación @CircuitBreaker:
Ejemplo con fallback:
El método fallback debe tener la misma signature que el método original más un parámetro adicional de tipo Exception
. Este método se ejecuta cuando el circuit breaker está abierto o cuando ocurre una excepción.
Ejemplo práctico completo
Creamos un controlador que demuestra el funcionamiento del circuit breaker:
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
try {
User user = userService.getUser(id);
return ResponseEntity.ok(user);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(null);
}
}
}
Cliente Feign con circuit breaker
Podemos combinar OpenFeign con circuit breaker de manera elegante:
@FeignClient(
name = "user-service",
url = "${user.service.url:http://localhost:8081}"
)
public interface UserFeignClient {
@GetMapping("/users/{id}")
User getUser(@PathVariable("id") Long id);
}
Configuración avanzada del circuit breaker
Para un control más granular, podemos configurar diferentes instancias:
resilience4j:
circuitbreaker:
instances:
userService:
failure-rate-threshold: 60
minimum-number-of-calls: 3
wait-duration-in-open-state: 20s
sliding-window-size: 8
permitted-number-of-calls-in-half-open-state: 2
productService:
failure-rate-threshold: 40
minimum-number-of-calls: 10
wait-duration-in-open-state: 45s
sliding-window-size: 15
Monitorización del circuit breaker
Resilience4j expone métricas que podemos consultar mediante actuator:
management:
endpoints:
web:
exposure:
include: health,circuitbreakers,circuitbreakerevents
endpoint:
health:
show-details: always
Accedemos a las métricas en: http://localhost:8080/actuator/circuitbreakers
Manejo de excepciones específicas
Podemos configurar qué excepciones deben contar como fallos:
@CircuitBreaker(
name = "userService",
fallbackMethod = "getUserFallback"
)
@TimeLimiter(name = "userService")
public User getUser(Long userId) {
return userFeignClient.getUser(userId);
}
En la configuración:
resilience4j:
circuitbreaker:
instances:
userService:
record-exceptions:
- java.net.ConnectException
- java.util.concurrent.TimeoutException
ignore-exceptions:
- com.example.BusinessException
Integración programática
También podemos usar el circuit breaker de forma programática sin anotaciones:
@Service
public class UserService {
private final CircuitBreakerRegistry circuitBreakerRegistry;
private final UserFeignClient userFeignClient;
public UserService(CircuitBreakerRegistry registry, UserFeignClient client) {
this.circuitBreakerRegistry = registry;
this.userFeignClient = client;
}
public User getUser(Long userId) {
CircuitBreaker circuitBreaker = circuitBreakerRegistry
.circuitBreaker("userService");
Supplier<User> decoratedSupplier = CircuitBreaker
.decorateSupplier(circuitBreaker,
() -> userFeignClient.getUser(userId));
return Try.ofSupplier(decoratedSupplier)
.recover(throwable -> getUserFallback(userId, throwable))
.get();
}
private User getUserFallback(Long userId, Throwable throwable) {
return User.builder()
.id(userId)
.name("Usuario no disponible")
.email("fallback@example.com")
.build();
}
}
Esta aproximación ofrece mayor control sobre el comportamiento del circuit breaker y es útil cuando necesitamos lógica más compleja.
Retry simple
El patrón Retry complementa perfectamente el circuit breaker proporcionando una segunda línea de defensa contra fallos temporales. Mientras que el circuit breaker previene llamadas repetidas cuando un servicio está claramente fallando, el retry maneja errores transitorios como timeouts de red, bloqueos momentáneos o picos de carga.
Resilience4j incluye un módulo de retry robusto que se integra perfectamente con Spring Cloud Circuit Breaker y puede combinarse con el circuit breaker que acabamos de configurar.
Configuración básica de retry
Para habilitar el retry, añadimos la configuración correspondiente en nuestro application.yml
:
resilience4j:
retry:
instances:
userService:
max-attempts: 3
wait-duration: 1s
exponential-backoff-multiplier: 2
retry-exceptions:
- java.net.ConnectException
- java.util.concurrent.TimeoutException
- feign.RetryableException
Las propiedades más importantes son:
- max-attempts: Número total de intentos (incluye el intento inicial)
- wait-duration: Tiempo de espera entre reintentos
- exponential-backoff-multiplier: Factor multiplicador para backoff exponencial
- retry-exceptions: Excepciones específicas que activan el retry
Implementación con anotaciones
La anotación @Retry se aplica de manera similar al circuit breaker:
@Service
public class OrderService {
@Autowired
private PaymentFeignClient paymentClient;
@Retry(name = "paymentService", fallbackMethod = "processPaymentFallback")
public PaymentResponse processPayment(PaymentRequest request) {
return paymentClient.processPayment(request);
}
public PaymentResponse processPaymentFallback(PaymentRequest request, Exception ex) {
return PaymentResponse.builder()
.transactionId("RETRY_FAILED_" + System.currentTimeMillis())
.status("FAILED")
.message("Servicio de pago temporalmente no disponible")
.build();
}
}
Combinando retry con circuit breaker
La combinación de ambos patrones proporciona una protección multicapa muy efectiva:
@Service
public class ProductService {
@Autowired
private InventoryFeignClient inventoryClient;
@CircuitBreaker(name = "inventoryService", fallbackMethod = "checkStockFallback")
@Retry(name = "inventoryService")
public StockResponse checkStock(String productId) {
return inventoryClient.checkStock(productId);
}
public StockResponse checkStockFallback(String productId, Exception ex) {
return StockResponse.builder()
.productId(productId)
.available(false)
.quantity(0)
.message("Stock no disponible temporalmente")
.build();
}
}
En este caso, primero se ejecutan los reintentos automáticos, y si todos fallan, se evalúa el circuit breaker.
Configuración de backoff exponencial
El backoff exponencial es crucial para evitar sobrecargar un servicio que ya está experimentando problemas:
resilience4j:
retry:
instances:
inventoryService:
max-attempts: 4
wait-duration: 500ms
exponential-backoff-multiplier: 1.5
randomized-wait-factor: 0.1
notificationService:
max-attempts: 2
wait-duration: 2s
exponential-backoff-multiplier: 3
Con esta configuración, los tiempos de espera para inventoryService
serían aproximadamente:
- Primer retry: ~500ms
- Segundo retry: ~750ms
- Tercer retry: ~1125ms
Retry condicional
Podemos configurar el retry para que solo se active con ciertas condiciones:
@Service
public class EmailService {
@Autowired
private EmailProviderClient emailClient;
@Retry(name = "emailService", fallbackMethod = "sendEmailFallback")
public void sendEmail(EmailRequest request) {
emailClient.sendEmail(request);
}
public void sendEmailFallback(EmailRequest request, Exception ex) {
// Log del error y posible envío a cola para procesamiento posterior
log.warn("Failed to send email after retries: {}", request.getSubject(), ex);
// Aquí podríamos enviar el email a una cola para procesamiento diferido
}
}
Con la configuración correspondiente:
resilience4j:
retry:
instances:
emailService:
max-attempts: 5
wait-duration: 3s
retry-exceptions:
- java.net.SocketTimeoutException
- java.net.ConnectException
ignore-exceptions:
- com.example.InvalidEmailException
- java.lang.IllegalArgumentException
Implementación programática de retry
Para casos que requieren mayor control, podemos usar la API programática:
@Service
public class ReportService {
private final RetryRegistry retryRegistry;
private final ReportGeneratorClient reportClient;
public ReportService(RetryRegistry retryRegistry, ReportGeneratorClient reportClient) {
this.retryRegistry = retryRegistry;
this.reportClient = reportClient;
}
public ReportData generateReport(String reportId) {
Retry retry = retryRegistry.retry("reportService");
Supplier<ReportData> decoratedSupplier = Retry.decorateSupplier(
retry, () -> reportClient.generateReport(reportId)
);
return Try.ofSupplier(decoratedSupplier)
.recover(throwable -> {
log.error("Report generation failed after retries: {}", reportId, throwable);
return ReportData.empty(reportId);
})
.get();
}
}
Métricas y monitorización de retry
Las métricas de retry nos ayudan a identificar servicios problemáticos:
management:
endpoints:
web:
exposure:
include: health,retries,retryevents
endpoint:
health:
show-details: always
Podemos consultar las métricas en: http://localhost:8080/actuator/retries
Configuración avanzada con predicados
Para escenarios complejos, podemos usar predicados personalizados:
@Configuration
public class RetryConfig {
@Bean
public Retry customRetry() {
return Retry.of("customService", RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofSeconds(1))
.retryOnResult(response -> response == null)
.retryExceptions(ConnectException.class, TimeoutException.class)
.ignoreExceptions(IllegalArgumentException.class)
.build());
}
}
Ejemplo práctico: servicio de autenticación
Un caso real donde el retry es especialmente útil:
@Service
public class AuthenticationService {
@Autowired
private AuthProviderClient authClient;
@Retry(name = "authService", fallbackMethod = "authenticateFallback")
@CircuitBreaker(name = "authService")
public AuthResponse authenticate(LoginRequest request) {
return authClient.authenticate(request);
}
public AuthResponse authenticateFallback(LoginRequest request, Exception ex) {
// En caso de fallo, podríamos permitir acceso limitado o usar cache local
return AuthResponse.builder()
.success(false)
.message("Servicio de autenticación temporalmente no disponible")
.allowedActions(List.of("READ_ONLY"))
.build();
}
}
Esta implementación combina ambos patrones para proporcionar una experiencia de usuario robusta incluso cuando los servicios de backend experimentan problemas temporales.
Fuentes y referencias
Documentación oficial y recursos externos para profundizar en SpringBoot
Documentación oficial de SpringBoot
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, SpringBoot es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de SpringBoot
Explora más contenido relacionado con SpringBoot y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
- Comprender el funcionamiento y estados del patrón Circuit Breaker.
- Configurar Resilience4j en proyectos Spring Boot para circuit breaker y retry.
- Implementar circuit breaker y retry mediante anotaciones y programación imperativa.
- Combinar circuit breaker y retry para protección multicapa en servicios remotos.
- Monitorizar y ajustar métricas y excepciones para optimizar la resiliencia del sistema.
Cursos que incluyen esta lección
Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje