Retour aux articles

Concevoir des systèmes résilients avec les Design Patterns (Circuit Breaker, Retry, Bulkhead) et Resilience4j en Spring Boot

Concevoir des systèmes résilients avec les Design Patterns (Circuit Breaker, Retry, Bulkhead) et Resilience4j en Spring Boot

Introduction

Construire des systèmes distribués robustes nécessite d'anticiper les pannes et d'appliquer des patterns de résilience. Dans cet article nous expliquons les patterns essentiels — Circuit Breaker, Retry et Bulkhead — et montrons comment les implémenter en Spring Boot avec Resilience4j. Vous trouverez des exemples de configuration, d'annotations et d'utilisation programmatique ainsi que des bonnes pratiques.

Design patterns de résilience

Circuit Breaker

Le pattern Circuit Breaker protège votre système d'appels distants défaillants en ouvrant une "bascule" lorsque le taux d'échec dépasse un seuil. Lorsqu'il est ouvert, il court-circuite les appels et renvoie rapidement une réponse de secours ou une erreur contrôlée, évitant la surcharge des dépendances en défaut.

Retry

Le pattern Retry réessaie automatiquement des opérations échouées selon une stratégie (nombre d'essais, délai, backoff exponentiel). Il est utile pour des erreurs transitoires, mais doit être utilisé avec prudence : répéter des appels coûteux ou non-idempotents peut aggraver la charge.

Bulkhead

Le pattern Bulkhead isole les ressources (threads, connexions) par domaine ou par dépendance afin qu'une panne n'engloutisse pas toutes les ressources du processus. Il existe deux variantes courantes : semaphore bulkhead (limite de concurrence) et thread-pool bulkhead (file d'attente et pool dédié pour les tâches longues).

Resilience4j et Spring Boot

Dépendances

Ajoutez le starter adapté à votre version de Spring Boot. Pour Spring Boot 3 utilisez resilience4j-spring-boot3, pour Spring Boot 2 utilisez resilience4j-spring-boot2. N'oubliez pas Micrometer / Actuator pour l'observabilité.

<dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-spring-boot3</artifactId> </dependency>

Configuration (application.yml)

Exemple minimal de configuration pour un backend nommé backendA :

resilience4j: circuitbreaker: instances: backendA: registerHealthIndicator: true slidingWindowSize: 100 minimumNumberOfCalls: 10 failureRateThreshold: 50 waitDurationInOpenState: 60s retry: instances: backendA: maxAttempts: 3 waitDuration: 1s enableExponentialBackoff: true exponentialBackoffMultiplier: 2 bulkhead: instances: backendA: maxConcurrentCalls: 10 maxWaitDuration: 0

Utilisation par annotations

Resilience4j propose des annotations pratiques pour l'intégration Spring :

import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; import io.github.resilience4j.retry.annotation.Retry; import io.github.resilience4j.bulkhead.annotation.Bulkhead; import org.springframework.stereotype.Service; @Service public class RemoteService { @CircuitBreaker(name = "backendA", fallbackMethod = "fallback") @Retry(name = "backendA") @Bulkhead(name = "backendA") public String callRemote(String id) { // appel HTTP ou RPC vers un service externe return remoteClient.call(id); } public String fallback(String id, Throwable t) { // logique de repli return \"default-response\"; } }

ThreadPoolBulkhead pour appels asynchrones

Pour des tâches longues ou pour préserver les threads du serveur, utilisez le ThreadPoolBulkhead :

import io.github.resilience4j.bulkhead.annotation.ThreadPoolBulkhead; import java.util.concurrent.CompletableFuture; @ThreadPoolBulkhead(name = \"backendA\", fallbackMethod = \"bulkheadFallback\") public CompletableFuture callAsync(String id) { return CompletableFuture.supplyAsync(() -> remoteClient.call(id)); } public CompletableFuture bulkheadFallback(String id, Throwable t) { return CompletableFuture.completedFuture(\"fallback-async\"); }

Approche programmatique et décorateurs

Pour des cas dynamiques ou des flux fonctionnels, on peut composer les clients avec des décorateurs :

import io.github.resilience4j.circuitbreaker.CircuitBreaker; import io.github.resilience4j.retry.Retry; import io.github.resilience4j.decorators.Decorators; import java.util.function.Supplier; CircuitBreaker cb = circuitBreakerRegistry.circuitBreaker(\"backendA\"); Retry retry = retryRegistry.retry(\"backendA\"); Supplier supplier = () -> remoteClient.call(id); Supplier decorated = Decorators.ofSupplier(supplier) .withCircuitBreaker(cb) .withRetry(retry) .decorate(); try { String result = decorated.get(); } catch (Exception e) { // gérer l'erreur }

Bonnes pratiques et pièges à éviter

1. Définir des seuils réalistes pour le circuit breaker : taille de fenêtre et durée d'ouverture doivent refléter le SLA et le flux d'appels. 2. Toujours considérer l'idempotence avant d'activer des retries. 3. Combiner Bulkhead avant Retry pour éviter que des retries n'épuisent les ressources. 4. Mettre des timeouts explicites avant les retries.

Observabilité et tests

Activez l'intégration Micrometer/Actuator pour exposer des métriques (taux d'échec, nombre d'appels, temps en état OPEN) vers Prometheus/Grafana. Écrivez des tests unitaires en simulant des latences et erreurs pour valider l'ouverture des circuits et l'appel des fallbacks. Sur les tests d'intégration, mesurez l'impact des retries sur la latence globale.

Conclusion

Les patterns Circuit Breaker, Retry et Bulkhead sont complémentaires : bien orchestrés ils rendent vos services tolérants aux pannes et maintiennent la disponibilité. Resilience4j offre une intégration légère et flexible avec Spring Boot, permettant des configurations déclaratives, des annotations simples et des usages programmatiques pour des besoins avancés. Priorisez l'observabilité et testez les comportements en conditions réelles.

À propos de l'expert

Laty Gueye Samba est un développeur full stack basé à Dakar, passionné par l'architecture logicielle. Spécialiste des écosystèmes Java (Spring Boot) et Angular.