Capítulo 5: Comunicação Entre Serviços
🎯 Objetivos do Capítulo
- Compreender os modos de comunicação síncrona vs assíncrona
- Implementar chamadas REST com OpenFeign
- Aplicar padrões de resiliência com Resilience4j
- Dominar o Circuit Breaker, Retry, Rate Limiter
- Implementar fallbacks eficazes
1. Modos de Comunicação
1.1 Comunicação Síncrona
Características:
- Cliente aguarda a resposta
- Simples de implementar
- Acoplamento temporal (dependência da disponibilidade)
- Latência acumulada
1.2 Comunicação Assíncrona
Características:
- Fire and forget
- Desacoplamento temporal
- Mais resiliente
- Mais complexo (eventual consistency)
1.3 Quando Usar Qual?
| Caso de Uso | Síncrono | Assíncrono |
|---|---|---|
| Validação de usuário | ✅ | |
| Obter detalhes do produto | ✅ | |
| Criar um pedido | ✅ | |
| Enviar notificação | ✅ | |
| Atualizar analytics | ✅ | |
| Sincronizar dados | ✅ |
2. OpenFeign - Cliente HTTP Declarativo
2.1 O que é Feign?
Feign (agora OpenFeign) é um cliente HTTP declarativo que permite definir chamadas REST via interfaces Java.
// Sem Feign (RestTemplate)
String url = "http://user-service/api/users/" + userId;
User user = restTemplate.getForObject(url, User.class);
// Com Feign (declarativo)
User user = userClient.getUserById(userId);
2.2 Dependências Maven
<dependencies>
<!-- OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- Eureka Client (para descoberta) -->
<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 Ativar Feign
package com.example.orderservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients // Ativa Feign
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
2.4 Definir um 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") // Nome do serviço no 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;
// Construtores
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 e 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 Utilização
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) {
// Chamada síncrona ao User Service
UserDto user = userClient.getUserById(userId);
if (user == null) {
throw new RuntimeException("User not found: " + userId);
}
// Chamada síncrona ao Product Service
ProductDto product = productClient.getProductById(productId);
if (product == null) {
throw new RuntimeException("Product not found: " + productId);
}
// Criar o pedido
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 Configuração 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 Níveis de Logging Feign
| Nível | Descrição |
|---|---|
NONE | Nenhum log |
BASIC | Método, URL, status, tempo |
HEADERS | + Headers |
FULL | + Body request/response |
3. Resilience4j - Padrões de Resiliência
3.1 Por que Resiliência?
3.2 Dependências
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
<!-- Se quiser usar as anotações @CircuitBreaker, @Retry, etc. -->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
</dependency>
<!-- AOP para as anotações -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
4. Circuit Breaker Pattern
4.1 Estados do Circuito
4.2 Configuração YAML
resilience4j:
circuitbreaker:
configs:
default:
# Tamanho da janela deslizante
slidingWindowSize: 10
# Tipo de janela (COUNT_BASED ou TIME_BASED)
slidingWindowType: COUNT_BASED
# Número mínimo de chamadas antes do cálculo
minimumNumberOfCalls: 5
# Limite de falha para abrir (%)
failureRateThreshold: 50
# Duração em estado OPEN (ms)
waitDurationInOpenState: 10000
# Chamadas permitidas em HALF_OPEN
permittedNumberOfCallsInHalfOpenState: 3
# Transição automática OPEN -> HALF_OPEN
automaticTransitionFromOpenToHalfOpenEnabled: true
# Exceções consideradas como falha
recordExceptions:
- java.io.IOException
- java.util.concurrent.TimeoutException
- feign.FeignException
# Exceções ignoradas
ignoreExceptions:
- com.example.BusinessException
instances:
userService:
baseConfig: default
productService:
baseConfig: default
failureRateThreshold: 30
4.3 Com Anotações
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 - DEVE ter a mesma assinatura + Exception
public UserDto getUserFallback(Long userId, Exception ex) {
// Log do erro
log.warn("Circuit breaker acionado para user {}. Erro: {}",
userId, ex.getMessage());
// Retornar um valor padrão ou em cache
return new UserDto(userId, "unknown", "unknown@example.com",
"Unknown", "User");
}
}
4.4 Com 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 acionado. Causa: {}", 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; // Pessimista em caso de erro
}
};
}
}
Ativar fallbacks Feign:
spring:
cloud:
openfeign:
circuitbreaker:
enabled: true
5. Retry Pattern
5.1 Configuração
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 Com Anotações
@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 esgotado para product {}. Erro: {}",
productId, ex.getMessage());
throw new ServiceUnavailableException("Product service indisponível");
}
}
5.3 Combinar Retry e Circuit Breaker
@Service
public class UserServiceClient {
// A ordem é importante! Retry DEPOIS 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);
}
}
Ordem de execução:
- Chamada inicial
- Se falhar → Retry (até maxAttempts)
- Se todos os retries falharem → Circuit Breaker
- Se Circuit OPEN → Fallback imediato
6. Rate Limiter Pattern
6.1 Configuração
resilience4j:
ratelimiter:
configs:
default:
# Número de permissões por período
limitForPeriod: 10
# Duração do período
limitRefreshPeriod: 1s
# Timeout de espera por permissão
timeoutDuration: 500ms
instances:
userService:
baseConfig: default
limitForPeriod: 100
limitRefreshPeriod: 1s
6.2 Com Anotações
@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("Limite de requisições excedido");
throw new TooManyRequestsException("Limite de requisições excedido. Tente novamente mais tarde.");
}
}
7. Bulkhead Pattern
7.1 Conceito
O Bulkhead isola recursos para evitar que um serviço lento bloqueie tudo.
7.2 Configuração
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 Com Anotações
@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 Configuração
resilience4j:
timelimiter:
configs:
default:
timeoutDuration: 5s
cancelRunningFuture: true
instances:
userService:
baseConfig: default
timeoutDuration: 3s
8.2 Com Anotações
@Service
public class SlowServiceClient {
@TimeLimiter(name = "slowService", fallbackMethod = "timeoutFallback")
public CompletableFuture<Response> callSlowService() {
return CompletableFuture.supplyAsync(() -> {
// Chamada potencialmente lenta
return slowClient.call();
});
}
public CompletableFuture<Response> timeoutFallback(TimeoutException ex) {
return CompletableFuture.completedFuture(Response.timeout());
}
}
9. Combinando Todos os Padrões
9.1 Configuração 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 Serviço com Todos os Padrões
@Service
@Slf4j
public class ResilientUserService {
private final UserClient userClient;
public ResilientUserService(UserClient userClient) {
this.userClient = userClient;
}
// Ordem de execução: 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("Chamando user service para user: {}", userId);
return userClient.getUserById(userId);
});
}
public CompletableFuture<UserDto> getUserFallback(Long userId, Exception ex) {
log.warn("Fallback acionado para user: {}. Erro: {}",
userId, ex.getMessage());
return CompletableFuture.completedFuture(
new UserDto(userId, "fallback", "fallback@example.com", "Fallback", "User")
);
}
}
10. Monitoramento 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 Disponíveis
# Estado dos circuit breakers
curl http://localhost:8083/actuator/circuitbreakers
# Eventos dos circuit breakers
curl http://localhost:8083/actuator/circuitbreakerevents
# Métricas Prometheus
curl http://localhost:8083/actuator/prometheus | grep resilience4j
10.3 Resposta Exemplo
{
"circuitBreakers": {
"userService": {
"failureRate": "20.0%",
"slowCallRate": "0.0%",
"state": "CLOSED",
"bufferedCalls": 10,
"failedCalls": 2,
"slowCalls": 0,
"notPermittedCalls": 0
}
}
}
11. Resumo
Tabela Resumo
| Padrão | Problema Resolvido | Quando Usar |
|---|---|---|
| Circuit Breaker | Efeito cascata de falhas | Chamadas externas instáveis |
| Retry | Erros transitórios | Timeouts de rede, 503 |
| Rate Limiter | Sobrecarga | APIs externas com quotas |
| Bulkhead | Esgotamento de recursos | Isolar serviços lentos |
| Time Limiter | Chamadas bloqueantes | Serviços potencialmente lentos |
Ordem dos Padrões
Request → Bulkhead → TimeLimiter → RateLimiter → CircuitBreaker → Retry → Service
Configuração Recomendada para Produção
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 Validação
Pergunta 1
Qual a diferença entre @FeignClient(fallback) e @FeignClient(fallbackFactory)?
Ver resposta
- fallback: Classe estática, não tem acesso à exceção
- fallbackFactory: Recebe a exceção como parâmetro, permite logar ou variar o fallback conforme o erro
Pergunta 2
Em que ordem são executados Retry e CircuitBreaker?
Ver resposta
Retry é executado PRIMEIRO (mais próximo do serviço), depois CircuitBreaker.
Se todos os retries falharem, então o CircuitBreaker conta uma falha. Se o CircuitBreaker está OPEN, os retries nem são tentados.