Aller au contenu principal

Chapitre 5: Communication Inter-Services

🎯 Objectifs du Chapitre​

  • Comprendre les modes de communication synchrone vs asynchrone
  • ImplĂ©menter des appels REST avec OpenFeign
  • Appliquer les patterns de rĂ©silience avec Resilience4j
  • MaĂźtriser le Circuit Breaker, Retry, Rate Limiter
  • ImplĂ©menter des fallbacks efficaces

1. Modes de Communication​

1.1 Communication Synchrone​

Caractéristiques:

  • Client attend la rĂ©ponse
  • Simple Ă  implĂ©menter
  • Couplage temporel (dĂ©pendance sur la disponibilitĂ©)
  • Latence cumulĂ©e

1.2 Communication Asynchrone​

Caractéristiques:

  • Fire and forget
  • DĂ©couplage temporel
  • Plus rĂ©silient
  • Plus complexe (eventual consistency)

1.3 Quand Utiliser Quoi?​

Cas d'UsageSynchroneAsynchrone
Validation utilisateur✅
Obtenir dĂ©tails produit✅
CrĂ©er une commande✅
Envoyer notification✅
Mettre à jour analytics✅
Synchroniser donnĂ©es✅

2. OpenFeign - Client HTTP DĂ©claratif​

2.1 Qu'est-ce que Feign?​

Feign (maintenant OpenFeign) est un client HTTP déclaratif qui permet de définir des appels REST via des interfaces Java.

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

// Avec Feign (déclaratif)
User user = userClient.getUserById(userId);

2.2 DĂ©pendances Maven​

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

<!-- Eureka Client (pour la découverte) -->
<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 Activer Feign​

package com.example.orderservice;

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

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

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

2.4 DĂ©finir 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") // Nom du service dans 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;

// Constructeurs
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 et 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 Utilisation​

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) {
// Appel synchrone au User Service
UserDto user = userClient.getUserById(userId);
if (user == null) {
throw new RuntimeException("User not found: " + userId);
}

// Appel synchrone au Product Service
ProductDto product = productClient.getProductById(productId);
if (product == null) {
throw new RuntimeException("Product not found: " + productId);
}

// Créer la commande
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 Configuration 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 Niveaux de Logging Feign​

NiveauDescription
NONEAucun log
BASICMéthode, URL, status, temps
HEADERS+ Headers
FULL+ Body request/response

3. Resilience4j - Patterns de RĂ©silience​

3.1 Pourquoi la RĂ©silience?​

3.2 DĂ©pendances​

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

<!-- Si vous voulez les annotations @CircuitBreaker, @Retry, etc. -->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
</dependency>

<!-- AOP pour les annotations -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

4. Circuit Breaker Pattern​

4.1 États du Circuit​

4.2 Configuration YAML​

resilience4j:
circuitbreaker:
configs:
default:
# Taille de la fenĂȘtre glissante
slidingWindowSize: 10
# Type de fenĂȘtre (COUNT_BASED ou TIME_BASED)
slidingWindowType: COUNT_BASED
# Nombre minimum d'appels avant calcul
minimumNumberOfCalls: 5
# Seuil d'échec pour ouvrir (%)
failureRateThreshold: 50
# Durée en état OPEN (ms)
waitDurationInOpenState: 10000
# Appels permis en HALF_OPEN
permittedNumberOfCallsInHalfOpenState: 3
# Transition auto OPEN -> HALF_OPEN
automaticTransitionFromOpenToHalfOpenEnabled: true
# Exceptions considérées comme échecs
recordExceptions:
- java.io.IOException
- java.util.concurrent.TimeoutException
- feign.FeignException
# Exceptions ignorées
ignoreExceptions:
- com.example.BusinessException

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

4.3 Avec Annotations​

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 - DOIT avoir la mĂȘme signature + Exception
public UserDto getUserFallback(Long userId, Exception ex) {
// Log l'erreur
log.warn("Circuit breaker triggered for user {}. Error: {}",
userId, ex.getMessage());

// Retourner une valeur par défaut ou cached
return new UserDto(userId, "unknown", "unknown@example.com",
"Unknown", "User");
}
}

4.4 Avec 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; // Pessimiste en cas d'erreur
}
};
}
}

Activer les fallbacks Feign:

spring:
cloud:
openfeign:
circuitbreaker:
enabled: true

5. Retry Pattern​

5.1 Configuration​

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 Avec Annotations​

@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 Combiner Retry et Circuit Breaker​

@Service
public class UserServiceClient {

// L'ordre est important! Retry PUIS 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);
}
}

Ordre d'exécution:

  1. Appel initial
  2. Si Ă©chec → Retry (jusqu'Ă  maxAttempts)
  3. Si tous les retries Ă©chouent → Circuit Breaker
  4. Si Circuit OPEN → Fallback immĂ©diat

6. Rate Limiter Pattern​

6.1 Configuration​

resilience4j:
ratelimiter:
configs:
default:
# Nombre de permissions par période
limitForPeriod: 10
# Durée de la période
limitRefreshPeriod: 1s
# Timeout d'attente pour une permission
timeoutDuration: 500ms

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

6.2 Avec Annotations​

@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 Concept​

Le Bulkhead isole les ressources pour éviter qu'un service lent ne bloque tout.

7.2 Configuration​

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 Avec Annotations​

@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 Configuration​

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

instances:
userService:
baseConfig: default
timeoutDuration: 3s

8.2 Avec Annotations​

@Service
public class SlowServiceClient {

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

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

9. Combiner Tous les Patterns​

9.1 Configuration Complùte​

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 Service avec Tous les Patterns​

@Service
@Slf4j
public class ResilientUserService {

private final UserClient userClient;

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

// Ordre d'exĂ©cution: 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​

# État des circuit breakers
curl http://localhost:8083/actuator/circuitbreakers

# ÉvĂ©nements des circuit breakers
curl http://localhost:8083/actuator/circuitbreakerevents

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

10.3 RĂ©ponse Exemple​

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

11. RĂ©sumé​

Tableau RĂ©capitulatif​

PatternProblÚme RésoluQuand l'Utiliser
Circuit BreakerCascade de pannesAppels externes instables
RetryErreurs transitoiresTimeouts réseau, 503
Rate LimiterSurchargeAPI externes avec quotas
BulkheadÉpuisement ressourcesIsoler les services lents
Time LimiterAppels bloquantsServices potentiellement lents

Ordre des Patterns​

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

Configuration RecommandĂ©e Production​

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 Validation​

Question 1​

Quelle est la différence entre @FeignClient(fallback) et @FeignClient(fallbackFactory)?

Voir la réponse
  • fallback: Classe statique, pas accĂšs Ă  l'exception
  • fallbackFactory: Reçoit l'exception en paramĂštre, permet de logger ou de varier le fallback selon l'erreur

Question 2​

Dans quel ordre s'exécutent Retry et CircuitBreaker?

Voir la réponse

Retry s'exécute EN PREMIER (plus proche du service), puis CircuitBreaker.

Si tous les retries Ă©chouent, alors le CircuitBreaker compte un Ă©chec. Si le CircuitBreaker est OPEN, les retries ne sont mĂȘme pas tentĂ©s.


Passer aux Travaux Pratiques →

Retour au Module 13