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 Uso | Sincrónico | Asincró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
| Nivel | Descripción |
|---|---|
NONE | Sin logs |
BASIC | Mé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:
- Llamada inicial
- Si falla → Retry (hasta maxAttempts)
- Si todos los retries fallan → Circuit Breaker
- 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ón | Problema Resuelto | Cuándo Usarlo |
|---|---|---|
| Circuit Breaker | Cascada de fallos | Llamadas externas inestables |
| Retry | Errores transitorios | Timeouts de red, 503 |
| Rate Limiter | Sobrecarga | APIs externas con cuotas |
| Bulkhead | Agotamiento de recursos | Aislar servicios lentos |
| Time Limiter | Llamadas bloqueantes | Servicios 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.