Saltar al contenido principal

Capítulo 5: Comunicación entre Servicios

🎯 Objetivos del Capítulo​

  • Comprender los modos de comunicación sincrónica vs asincrónica
  • Implementar llamadas REST con OpenFeign
  • Aplicar patrones de resiliencia con Resilience4j
  • Dominar Circuit Breaker, Retry, Rate Limiter
  • Implementar fallbacks efectivos

1. Modos de Comunicación​

1.1 Comunicación Sincrónica​

Características:

  • El cliente espera la respuesta
  • Simple de implementar
  • Acoplamiento temporal (dependencia de la disponibilidad)
  • Latencia acumulada

1.2 Comunicación Asincrónica​

Características:

  • Fire and forget
  • Desacoplamiento temporal
  • Más resiliente
  • Más complejo (consistencia eventual)

1.3 ¿Cuándo Usar Cada Uno?​

Caso de UsoSincrónicoAsincrónico
Validación de usuario✅
Obtener detalles de producto✅
Crear una orden✅
Enviar notificación✅
Actualizar analytics✅
Sincronizar datos✅

2. OpenFeign - Cliente HTTP Declarativo​

2.1 ¿Qué es Feign?​

Feign (ahora OpenFeign) es un cliente HTTP declarativo que permite definir llamadas REST a través de interfaces Java.

// Sin Feign (RestTemplate)
String url = "http://user-service/api/users/" + userId;
User user = restTemplate.getForObject(url, User.class);

// Con Feign (declarativo)
User user = userClient.getUserById(userId);

2.2 Dependencias Maven​

<dependencies>
<!-- OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

<!-- Eureka Client (para descubrimiento) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

<!-- Resilience4j -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
</dependencies>

2.3 Activar Feign​

package com.example.orderservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients // Activa Feign
public class OrderServiceApplication {

public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}

2.4 Definir un Feign Client​

package com.example.orderservice.client;

import com.example.orderservice.dto.UserDto;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

import java.util.List;

@FeignClient(name = "user-service") // Nombre del servicio en Eureka
public interface UserClient {

@GetMapping("/api/users/{id}")
UserDto getUserById(@PathVariable("id") Long id);

@GetMapping("/api/users")
List<UserDto> getAllUsers();

@GetMapping("/api/users/{id}/exists")
Boolean userExists(@PathVariable("id") Long id);
}

2.5 DTO​

package com.example.orderservice.dto;

public class UserDto {
private Long id;
private String username;
private String email;
private String firstName;
private String lastName;

// Constructores
public UserDto() {}

public UserDto(Long id, String username, String email,
String firstName, String lastName) {
this.id = id;
this.username = username;
this.email = email;
this.firstName = firstName;
this.lastName = lastName;
}

// Getters y Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }

public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }

public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }

public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }

public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
}

2.6 Uso​

package com.example.orderservice.service;

import com.example.orderservice.client.UserClient;
import com.example.orderservice.client.ProductClient;
import com.example.orderservice.dto.UserDto;
import com.example.orderservice.dto.ProductDto;
import com.example.orderservice.model.Order;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

private final UserClient userClient;
private final ProductClient productClient;
private final OrderRepository orderRepository;

public OrderService(UserClient userClient,
ProductClient productClient,
OrderRepository orderRepository) {
this.userClient = userClient;
this.productClient = productClient;
this.orderRepository = orderRepository;
}

public Order createOrder(Long userId, Long productId, int quantity) {
// Llamada sincrónica al User Service
UserDto user = userClient.getUserById(userId);
if (user == null) {
throw new RuntimeException("User not found: " + userId);
}

// Llamada sincrónica al Product Service
ProductDto product = productClient.getProductById(productId);
if (product == null) {
throw new RuntimeException("Product not found: " + productId);
}

// Crear la orden
Order order = new Order();
order.setUserId(userId);
order.setProductId(productId);
order.setQuantity(quantity);
order.setTotalPrice(product.getPrice() * quantity);
order.setUserEmail(user.getEmail());

return orderRepository.save(order);
}
}

2.7 Configuración Feign​

spring:
cloud:
openfeign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 10000
loggerLevel: BASIC
user-service:
connectTimeout: 3000
readTimeout: 5000
loggerLevel: FULL

# Logging Feign
logging:
level:
com.example.orderservice.client: DEBUG

2.8 Niveles de Logging Feign​

NivelDescripción
NONESin logs
BASICMétodo, URL, status, tiempo
HEADERS+ Headers
FULL+ Body request/response

3. Resilience4j - Patrones de Resiliencia​

3.1 ¿Por qué la Resiliencia?​

3.2 Dependencias​

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>

<!-- Si desea las anotaciones @CircuitBreaker, @Retry, etc. -->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
</dependency>

<!-- AOP para las anotaciones -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

4. Circuit Breaker Pattern​

4.1 Estados del Circuito​

4.2 Configuración YAML​

resilience4j:
circuitbreaker:
configs:
default:
# Tamaño de la ventana deslizante
slidingWindowSize: 10
# Tipo de ventana (COUNT_BASED o TIME_BASED)
slidingWindowType: COUNT_BASED
# Número mínimo de llamadas antes de calcular
minimumNumberOfCalls: 5
# Umbral de fallo para abrir (%)
failureRateThreshold: 50
# Duración en estado OPEN (ms)
waitDurationInOpenState: 10000
# Llamadas permitidas en HALF_OPEN
permittedNumberOfCallsInHalfOpenState: 3
# Transición automática OPEN -> HALF_OPEN
automaticTransitionFromOpenToHalfOpenEnabled: true
# Excepciones consideradas como fallos
recordExceptions:
- java.io.IOException
- java.util.concurrent.TimeoutException
- feign.FeignException
# Excepciones ignoradas
ignoreExceptions:
- com.example.BusinessException

instances:
userService:
baseConfig: default
productService:
baseConfig: default
failureRateThreshold: 30

4.3 Con Anotaciones​

package com.example.orderservice.service;

import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.springframework.stereotype.Service;

@Service
public class UserServiceClient {

private final UserClient userClient;

public UserServiceClient(UserClient userClient) {
this.userClient = userClient;
}

@CircuitBreaker(name = "userService", fallbackMethod = "getUserFallback")
public UserDto getUser(Long userId) {
return userClient.getUserById(userId);
}

// Fallback - DEBE tener la misma firma + Exception
public UserDto getUserFallback(Long userId, Exception ex) {
// Registrar el error
log.warn("Circuit breaker triggered for user {}. Error: {}",
userId, ex.getMessage());

// Retornar un valor por defecto o en caché
return new UserDto(userId, "unknown", "unknown@example.com",
"Unknown", "User");
}
}

4.4 Con Feign (Fallback Factory)​

package com.example.orderservice.client;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

@FeignClient(
name = "user-service",
fallbackFactory = UserClientFallbackFactory.class
)
public interface UserClient {

@GetMapping("/api/users/{id}")
UserDto getUserById(@PathVariable("id") Long id);

@GetMapping("/api/users/{id}/exists")
Boolean userExists(@PathVariable("id") Long id);
}
package com.example.orderservice.client;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;

@Component
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {

private static final Logger log = LoggerFactory.getLogger(UserClientFallbackFactory.class);

@Override
public UserClient create(Throwable cause) {
log.error("UserClient fallback triggered. Cause: {}", cause.getMessage());

return new UserClient() {
@Override
public UserDto getUserById(Long id) {
return new UserDto(id, "fallback-user", "fallback@example.com",
"Fallback", "User");
}

@Override
public Boolean userExists(Long id) {
return false; // Pesimista en caso de error
}
};
}
}

Activar los fallbacks Feign:

spring:
cloud:
openfeign:
circuitbreaker:
enabled: true

5. Retry Pattern​

5.1 Configuración​

resilience4j:
retry:
configs:
default:
maxAttempts: 3
waitDuration: 1000
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2
retryExceptions:
- java.io.IOException
- java.util.concurrent.TimeoutException
- feign.RetryableException
ignoreExceptions:
- com.example.BusinessException

instances:
userService:
baseConfig: default
maxAttempts: 5

5.2 Con Anotaciones​

@Service
public class ProductServiceClient {

@Retry(name = "productService", fallbackMethod = "getProductFallback")
public ProductDto getProduct(Long productId) {
return productClient.getProductById(productId);
}

public ProductDto getProductFallback(Long productId, Exception ex) {
log.warn("Retry exhausted for product {}. Error: {}",
productId, ex.getMessage());
throw new ServiceUnavailableException("Product service unavailable");
}
}

5.3 Combinar Retry y Circuit Breaker​

@Service
public class UserServiceClient {

// ¡El orden es importante! Retry LUEGO CircuitBreaker
@Retry(name = "userService")
@CircuitBreaker(name = "userService", fallbackMethod = "getUserFallback")
public UserDto getUser(Long userId) {
return userClient.getUserById(userId);
}

public UserDto getUserFallback(Long userId, Exception ex) {
return getCachedUser(userId);
}
}

Orden de ejecución:

  1. Llamada inicial
  2. Si falla → Retry (hasta maxAttempts)
  3. Si todos los retries fallan → Circuit Breaker
  4. Si Circuit OPEN → Fallback inmediato

6. Rate Limiter Pattern​

6.1 Configuración​

resilience4j:
ratelimiter:
configs:
default:
# Número de permisos por período
limitForPeriod: 10
# Duración del período
limitRefreshPeriod: 1s
# Timeout de espera para un permiso
timeoutDuration: 500ms

instances:
userService:
baseConfig: default
limitForPeriod: 100
limitRefreshPeriod: 1s

6.2 Con Anotaciones​

@Service
public class ExternalApiClient {

@RateLimiter(name = "externalApi", fallbackMethod = "rateLimitFallback")
public Response callExternalApi(Request request) {
return externalClient.call(request);
}

public Response rateLimitFallback(Request request, RequestNotPermitted ex) {
log.warn("Rate limit exceeded");
throw new TooManyRequestsException("Rate limit exceeded. Try again later.");
}
}

7. Bulkhead Pattern​

7.1 Concepto​

El Bulkhead aísla los recursos para evitar que un servicio lento bloquee todo.

7.2 Configuración​

resilience4j:
bulkhead:
configs:
default:
maxConcurrentCalls: 10
maxWaitDuration: 500ms

instances:
userService:
baseConfig: default
maxConcurrentCalls: 5

thread-pool-bulkhead:
configs:
default:
maxThreadPoolSize: 10
coreThreadPoolSize: 5
queueCapacity: 20

instances:
productService:
baseConfig: default

7.3 Con Anotaciones​

@Service
public class OrderService {

@Bulkhead(name = "userService", type = Bulkhead.Type.SEMAPHORE)
public UserDto getUser(Long userId) {
return userClient.getUserById(userId);
}

@Bulkhead(name = "productService", type = Bulkhead.Type.THREADPOOL)
public CompletableFuture<ProductDto> getProductAsync(Long productId) {
return CompletableFuture.supplyAsync(() ->
productClient.getProductById(productId)
);
}
}

8. Time Limiter Pattern​

8.1 Configuración​

resilience4j:
timelimiter:
configs:
default:
timeoutDuration: 5s
cancelRunningFuture: true

instances:
userService:
baseConfig: default
timeoutDuration: 3s

8.2 Con Anotaciones​

@Service
public class SlowServiceClient {

@TimeLimiter(name = "slowService", fallbackMethod = "timeoutFallback")
public CompletableFuture<Response> callSlowService() {
return CompletableFuture.supplyAsync(() -> {
// Llamada potencialmente lenta
return slowClient.call();
});
}

public CompletableFuture<Response> timeoutFallback(TimeoutException ex) {
return CompletableFuture.completedFuture(Response.timeout());
}
}

9. Combinar Todos los Patrones​

9.1 Configuración Completa​

resilience4j:
circuitbreaker:
instances:
userService:
slidingWindowSize: 10
failureRateThreshold: 50
waitDurationInOpenState: 10000
permittedNumberOfCallsInHalfOpenState: 3

retry:
instances:
userService:
maxAttempts: 3
waitDuration: 500ms
enableExponentialBackoff: true

ratelimiter:
instances:
userService:
limitForPeriod: 100
limitRefreshPeriod: 1s

bulkhead:
instances:
userService:
maxConcurrentCalls: 10

timelimiter:
instances:
userService:
timeoutDuration: 3s

9.2 Servicio con Todos los Patrones​

@Service
@Slf4j
public class ResilientUserService {

private final UserClient userClient;

public ResilientUserService(UserClient userClient) {
this.userClient = userClient;
}

// Orden de ejecución: Bulkhead → TimeLimiter → RateLimiter → CircuitBreaker → Retry
@Bulkhead(name = "userService")
@TimeLimiter(name = "userService")
@RateLimiter(name = "userService")
@CircuitBreaker(name = "userService", fallbackMethod = "getUserFallback")
@Retry(name = "userService")
public CompletableFuture<UserDto> getUser(Long userId) {
return CompletableFuture.supplyAsync(() -> {
log.info("Calling user service for user: {}", userId);
return userClient.getUserById(userId);
});
}

public CompletableFuture<UserDto> getUserFallback(Long userId, Exception ex) {
log.warn("Fallback triggered for user: {}. Error: {}",
userId, ex.getMessage());
return CompletableFuture.completedFuture(
new UserDto(userId, "fallback", "fallback@example.com", "Fallback", "User")
);
}
}

10. Monitoring Resilience4j​

10.1 Actuator Endpoints​

management:
endpoints:
web:
exposure:
include: health,circuitbreakers,circuitbreakerevents,retries,ratelimiters
endpoint:
health:
show-details: always
health:
circuitbreakers:
enabled: true
ratelimiters:
enabled: true

10.2 Endpoints Disponibles​

# Estado de los circuit breakers
curl http://localhost:8083/actuator/circuitbreakers

# Eventos de los circuit breakers
curl http://localhost:8083/actuator/circuitbreakerevents

# Métricas Prometheus
curl http://localhost:8083/actuator/prometheus | grep resilience4j

10.3 Respuesta de Ejemplo​

{
"circuitBreakers": {
"userService": {
"failureRate": "20.0%",
"slowCallRate": "0.0%",
"state": "CLOSED",
"bufferedCalls": 10,
"failedCalls": 2,
"slowCalls": 0,
"notPermittedCalls": 0
}
}
}

11. Resumen​

Tabla Resumen​

PatrónProblema ResueltoCuándo Usarlo
Circuit BreakerCascada de fallosLlamadas externas inestables
RetryErrores transitoriosTimeouts de red, 503
Rate LimiterSobrecargaAPIs externas con cuotas
BulkheadAgotamiento de recursosAislar servicios lentos
Time LimiterLlamadas bloqueantesServicios potencialmente lentos

Orden de los Patrones​

Request → Bulkhead → TimeLimiter → RateLimiter → CircuitBreaker → Retry → Service

Configuración Recomendada para Producción​

resilience4j:
circuitbreaker:
instances:
default:
slidingWindowSize: 100
failureRateThreshold: 50
waitDurationInOpenState: 30000
permittedNumberOfCallsInHalfOpenState: 10

retry:
instances:
default:
maxAttempts: 3
waitDuration: 1000ms
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2

ratelimiter:
instances:
default:
limitForPeriod: 1000
limitRefreshPeriod: 1s

12. Quiz de Validación​

Pregunta 1​

¿Cuál es la diferencia entre @FeignClient(fallback) y @FeignClient(fallbackFactory)?

Ver la respuesta
  • fallback: Clase estática, no tiene acceso a la excepción
  • fallbackFactory: Recibe la excepción como parámetro, permite registrar logs o variar el fallback según el error

Pregunta 2​

¿En qué orden se ejecutan Retry y CircuitBreaker?

Ver la respuesta

Retry se ejecuta PRIMERO (más cerca del servicio), luego CircuitBreaker.

Si todos los retries fallan, entonces el CircuitBreaker cuenta un fallo. Si el CircuitBreaker está OPEN, los retries ni siquiera se intentan.


Pasar a los Trabajos Prácticos →

Volver al Módulo 13