Pular para o conteúdo principal

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 UsoSíncronoAssí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ívelDescrição
NONENenhum log
BASICMé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:

  1. Chamada inicial
  2. Se falhar → Retry (até maxAttempts)
  3. Se todos os retries falharem → Circuit Breaker
  4. 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ãoProblema ResolvidoQuando Usar
Circuit BreakerEfeito cascata de falhasChamadas externas instáveis
RetryErros transitóriosTimeouts de rede, 503
Rate LimiterSobrecargaAPIs externas com quotas
BulkheadEsgotamento de recursosIsolar serviços lentos
Time LimiterChamadas bloqueantesServiç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.


Ir para os Exercícios Práticos →

Voltar ao Módulo 13