Aller au contenu

Application à toute épreuve : transformez votre application en forteresse de résilience

Transformez votre application Spring Boot en véritable forteresse : testez sa résilience avec Resilience4j, exposez ses failles avec Chaos Monkey et validez sa robustesse sous charge avec Gatling, pour qu’elle continue à servir même quand tout vacille.

Une véritable forteresse applicative

Pourquoi une application “à toute épreuve” ?

Le nouveau contexte de la production moderne

Microservices, API tierces, bases partagées, réseaux capricieux, cloud qui scale… les pannes ne sont plus l’exception.
L’objectif n’est plus d’“éviter” l’incident, mais de maintenir un service acceptable même quand tout ne va pas bien.

Un service qui répond lentement peut asphyxier toute la chaîne.
Un composant en panne peut déclencher un effet de cascade.
Un pic de trafic peut rendre l’application indisponible alors qu’aucune erreur “fonctionnelle” ne s’est produite.

De l’application fragile à la forteresse de résilience

L’image de la forteresse résume bien l’idée : on ne se contente pas de mettre des try/catch.
On conçoit la résilience, on l’éprouve, puis on la mesure.

Dans cet article, on construit cette forteresse en trois couches :

  • Resilience4j pour la résilience par design (remparts)
  • Chaos Monkey pour l’expérimentation (béliers contrôlés)
  • Gatling pour valider sous charge (siège à grande échelle)

L’application d’exemple et le code support

Le fil rouge est une application Spring Boot de streaming vidéo (type Disney+/Netflix), basée sur le repo suivant :

GitHub - ErwanLT/chaos-monkey-application: Chaos Mokey et resilience demonstration application in java spring boot
Chaos Mokey et resilience demonstration application in java spring boot - ErwanLT/chaos-monkey-application

On y retrouve un contrôleur MVC, des services métiers, des dépendances externes simulées, une base H2 et des endpoints REST.
Le front est en Thymeleaf, ce qui permet d’illustrer aussi la partie “expérience utilisateur dégradée”.

Quelques briques clés du repo :
CatalogService, StreamingService, RecommendationService, UserService, des repositories JPA, une UI v1/v2, et des endpoints REST (/api/catalog, /api/streaming, /api/recommendations).
L’objectif est de partir de cette base et de la renforcer couche par couche.

Le trajet d’une requête (et où ça peut casser)

Prenons un parcours “classique” d’utilisateur :

  1. Il ouvre le catalogue (GET /api/catalog/videos)
  2. Il lance un stream (POST /api/streaming/start)
  3. Il récupère des recommandations (GET /api/recommendations/{userId})

Sur ce trajet, les points de casse sont multiples : base de données, latence réseau, surcharge CPU, services tiers simulés.
L’idée est simple : chaque point de casse mérite un mécanisme de résilience.

Architecture logique (vue simplifiée)

architecture simplifié du projet

L’article complète trois articles existants :

Introduisez du chaos dans votre application Spring Boot
Le chaos n’est pas une menace, mais un révélateur : il expose les failles et forge la résilience. Découvrez comment Chaos Monkey met votre application Spring Boot à l’épreuve pour la rendre plus robuste que jamais.

introduisez du chaos dans votre application spring boot

Résilience applicative avec Resilience4j et Spring Boot
Découvrez comment rendre vos applications Spring Boot plus résilientes avec Resilience4j : circuit breaker, retry, rate limiter, bulkhead et time limiter, expliqués clairement avec des exemples concrets et des bonnes pratiques pour tolérer les pannes.

Résilience applicative avec Resilience4J

Maîtrisez les Tests de Charge avec Gatling pour Spring Boot
Les applications Spring Boot nécessitent des tests de performance rigoureux pour garantir leur robustesse sous charge. Découvrez comment Gatling permet de simuler des scénarios réalistes et d’optimiser vos APIs.

Test de charge avec Gatling

L’objectif ici est de les relier dans un même parcours opérationnel.

Poser les fondations : concevoir la résilience avec Resilience4j

Identifier les points de fragilité de l’application

Dans l’application, plusieurs points sont critiques :

  • démarrage du streaming (StreamingService.startStream),
  • génération de recommandations (RecommendationService.generateRecommendations),
  • récupération de données (catalogue, historique, utilisateurs).

Ce sont des appels qui peuvent échouer, ralentir ou saturer.
On commence donc par les cartographier, puis on place des mécanismes de protection.

Exemples concrets de fragilité dans le repo :

  • démarrage de stream avec latence simulée (Thread.sleep(100)),\n- recommandations dépendantes de la base,\n- endpoints de streaming sensibles à la charge (lecture de ressources MP4).

Ce sont des “zones à risque” idéales pour introduire de la résilience.

Pour rendre cela explicite, on peut cartographier risques et contre‑mesures :

Zone fragile Risque Protection
StreamingService.startStream Latence / indisponibilité Timeout + Retry + CircuitBreaker + Fallback
RecommendationService Erreurs DB / surcharge CircuitBreaker + Fallback
Endpoints streaming Saturation threads Bulkhead + RateLimiter

Timeouts et isolation : ne pas subir la lenteur

Un service qui met 30 secondes à répondre ne doit jamais bloquer une requête utilisateur aussi longtemps.
La première brique de résilience, ce sont les timeouts.

Dans ce repo, on illustre le timeout via une exécution encapsulée :
on limite le temps d’un appel avec un Future.get(timeout), et on transforme l’expiration en exception métier.

private <T> T executeWithTimeout(String taskName, Supplier<T> supplier) {
    ExecutorService executor = Executors.newSingleThreadExecutor();
    try {
        log.info("[{}] ⏳ Début attente (Timeout réglé à 2s)...", taskName);
        Future<T> future = executor.submit(() -> {
            long start = System.currentTimeMillis();
            log.info("[{}] 🟢 Thread Worker démarré", taskName);
            T result = supplier.get();
            log.info("[{}] ✅ Service terminé proprement en {} ms", taskName,
                    (System.currentTimeMillis() - start));
            return result;
        });

        return future.get(2, TimeUnit.SECONDS);
    } catch (TimeoutException e) {
        log.error("[{}] 💥 TIMEOUT déclenché ! Fermeture forcée du thread.", taskName);
        throw new RequestTimeoutException("Le service a mis trop de temps.");
    } catch (InterruptedException | ExecutionException e) {
        log.error("[{}] ❌ Erreur : {}", taskName, e.getMessage());
        throw new RuntimeException("Erreur lors de l'exécution", e);
    } finally {
        executor.shutdownNow();
    }
}

gestion globale du timeOut

Ce n’est pas la seule approche possible, mais elle rend le timeout visible et testable.
Sans timeout, la latence se propage.
Avec un timeout, on borne la dégradation : la requête échoue vite, et on peut basculer sur un fallback.

💡 Pourquoi ce choix est volontairement simple

Cette approche est parfaite pour illustrer la mécanique du timeout et la réaction du système. Dans une vraie app, on préférera propager le timeout au niveau des clients HTTP (WebClient, RestClient), afin d’éviter de multiplier des threads dédiés.
Ici, l’objectif pédagogique est clair : voir le timeout, déclencher le fallback, mesurer l’impact.

Circuit Breaker : casser l’effet “marteau‑piqueur”

Le circuit breaker évite d’acharner un service déjà en panne.
Dans RecommendationService, il protège l’appel qui récupère les recommandations :

@CircuitBreaker(name = "recommendationService", fallbackMethod = "fallbackRecommendations")
public List<Recommendation> getRecommendationsForUser(Long userId) {
    log.info("Fetching recommendations for user: {}", userId);
    return recommendationRepository.findTop10ByUserIdOrderByScoreDesc(userId);
}

En cas de panne, le fallback renvoie un contenu dégradé mais utile :

public List<Recommendation> fallbackRecommendations(Long userId, Throwable t) {
    log.warn("Circuit breaker open or error fetching recommendations for user {}. Reason: {}", userId, t.getMessage());
    List<Video> popular = videoRepository.findTop10ByOrderByViewCountDesc();
    return popular.stream()
            .map(v -> new Recommendation(userId, v.getId(), 0.0, "Popular now (Fallback)"))
            .collect(Collectors.toList());
}

La configuration associée est déclarée dans application.properties :

resilience4j.circuitbreaker.instances.recommendationService.registerHealthIndicator=true
resilience4j.circuitbreaker.instances.recommendationService.slidingWindowSize=10
resilience4j.circuitbreaker.instances.recommendationService.failureRateThreshold=50
resilience4j.circuitbreaker.instances.recommendationService.waitDurationInOpenState=10s

configuration du circuitBreaker

Ce sont ces paramètres qui dictent quand le disjoncteur s’ouvre et combien de temps il reste ouvert.

Retry et backoff : encaisser les pannes transitoires

Les pannes transitoires (réseau, surcharge temporaire) peuvent souvent être absorbées par un retry contrôlé.
Dans StreamingService, un retry est appliqué sur le démarrage du stream :

@Retry(name = "streamingService", fallbackMethod = "fallbackStartStream")
@CircuitBreaker(name = "streamingService")
public Map<String, Object> startStream(Long userId, Long videoId) {
    // ...
}

La configuration du retry est centralisée dans application.properties :

resilience4j.retry.instances.recommendationService.maxAttempts=3
resilience4j.retry.instances.recommendationService.waitDuration=500ms

resilience4j.retry.instances.streamingService.maxAttempts=3
resilience4j.retry.instances.streamingService.waitDuration=1s

configuration du retry

On reste volontairement raisonnable sur le nombre de tentatives pour ne pas saturer un service déjà fragile.

2026-03-20T08:24:11.905+01:00  INFO 62729 --- Attempting to start stream for user: 1 and video: 15
2026-03-20T08:24:11.916+01:00  INFO 62729 --- Chaos Monkey - exception
2026-03-20T08:24:12.919+01:00  INFO 62729 --- Attempting to start stream for user: 1 and video: 15
2026-03-20T08:24:12.929+01:00  INFO 62729 --- Chaos Monkey - exception
2026-03-20T08:24:13.932+01:00  INFO 62729 --- Attempting to start stream for user: 1 and video: 15
2026-03-20T08:24:13.935+01:00  INFO 62729 --- Chaos Monkey - exception

on peut voir le retry en action dans les logs

Bulkhead et Rate Limiter : protéger les ressources internes

Même si un service externe tombe, il ne doit pas consommer tous les threads de l’application.
C’est exactement le rôle des bulkheads (sémaphores ou pools dédiés) et des rate limiters.

Dans ce repo, ils ne sont pas encore activés, mais l’architecture est prête.
On pourrait isoler les appels vers des dépendances externes en dédiant un pool par service.

L’idée est simple : même si RecommendationService s’effondre, StreamingService doit continuer à fonctionner.
La résilience ne sert à rien si un seul composant peut “tout faire tomber”.

Fallbacks : offrir une expérience dégradée mais contrôlée

La résilience n’est pas seulement “ne pas planter”.
C’est rendre un service acceptable même dégradé.

Dans StreamingService, le fallback fournit un statut explicite :

public Map<String, Object> fallbackStartStream(Long userId, Long videoId, Throwable t) {
    log.error("Streaming service error for user {} and video {}. Reason: {}", userId, videoId, t.getMessage());
    log.info("Returning unavailable status as fallback for stream request");
    Map<String, Object> streamInfo = new HashMap<>();
    streamInfo.put("userId", userId);
    streamInfo.put("videoId", videoId);
    streamInfo.put("streamUrl", "");
    streamInfo.put("quality", "N/A");
    streamInfo.put("status", "TEMPORARILY_UNAVAILABLE");
    streamInfo.put("error", t.getMessage());
    return streamInfo;
}

notre fallback en cas de problème de streaming

2026-03-20T08:24:13.937+01:00 ERROR 62729 --- Streaming service error for user 1 and video 15. Reason: Video not found
2026-03-20T08:24:13.937+01:00  INFO 62729 --- Returning unavailable status as fallback for stream request

suite des logs de retry, trop d'échec, on arrive dans le fallback

Pour l’utilisateur, la différence est majeure :
on ne renvoie pas une erreur opaque, on renvoie une réponse compréhensible et gérable côté UI.

Dans l’application, les pages d’erreur sont thématiques, ce qui renforce cette logique de “dégradation contrôlée”.

Mettre la forteresse à l’épreuve : Chaos Monkey en environnement contrôlé

Pourquoi le chaos est indispensable à la résilience

Sans expérimentation, la résilience reste théorique.
Le chaos engineering permet de valider en conditions contrôlées que les mécanismes de protection fonctionnent vraiment.

Brancher Chaos Monkey sur l’application Spring Boot

Le repo intègre chaos-monkey-spring-boot directement dans le module chaos-application :

<dependency>
    <groupId>de.codecentric</groupId>
    <artifactId>chaos-monkey-spring-boot</artifactId>
    <version>4.0.0</version>
</dependency>

la dépendance vers la version 4, ça y est on peut casser les applications Spring Boot 4

Un profil dédié active l’exposition des endpoints dans application-chaos-monkey.yaml :

management:
  endpoint:
    chaosmonkey:
      enabled: true
  endpoints:
    web:
      exposure:
        include: chaosmonkey, health, info, circuitbreakers, retries

extrait de notre application.yml

On lance l’application avec ce profil : chaos-monkey.

Pour en savoir plus sur les profils dans Spring Boot, c'est par ici :

Les profils dans Spring Boot
Les profils Spring Boot permettent d’adapter la configuration et le comportement d’une application selon l’environnement, sans modifier le code. Maîtriser application-xxx.properties est essentiel pour construire des applications claires, maintenables et fiables.

les profils dans spring boot, comment et pourquoi ?

Ce qui nous permet d'avoir dans la console au démarrage, la petite bannière chaos monkey :

ready to do evil

Nous pouvons désormais piloter le chaos via les actuators spring boot :

nos petits actuators disponibles depuis swagger

Simuler la latence : tester les timeouts et les retries

Scénario : injection de latence sur StreamingService.
On observe alors :

  • des timeouts côté client,
  • des retries Resilience4j,
  • et, si la panne persiste, le fallback.

Exemple simple pour déclencher un stream :

curl -X POST http://localhost:8080/api/streaming/start \\
  -H \"Content-Type: application/json\" \\
  -d '{\"userId\": 1, \"videoId\": 1}'
page d'erreur en cas de timeOut

Provoquer des erreurs : valider le circuit breaker

Scénario : injection d’exceptions sur les recommandations.
Le circuit breaker s’ouvre, et l’on bascule vers le fallback “contenu populaire”.

Exemple côté recommandations :

curl http://localhost:8080/api/recommendations/1

Quand le breaker s’ouvre, la réponse reste valide, mais le contenu devient “Popular now (Fallback)”.

2026-03-20T12:49:36.510+01:00  INFO 78533 --- Fetching recommendations for user: 2
2026-03-20T12:49:36.511+01:00  INFO 78533 --- Circuit breaker open or error fetching recommendations for user 2. Reason: Simulation d’erreur pour ouvrir le CB dès la première tentative
2026-03-20T12:49:36.512+01:00  INFO 78533 --- Returning popular content as fallback recommendations for user 2

Documenter les scénarios de chaos dans le code

Les scénarios doivent être reproductibles.
L’idée est de documenter clairement :

  • les endpoints Actuators utilisés,
  • les types de pannes injectées,
  • la réaction attendue (fallback, breaker, logs).

Une bonne pratique consiste à conserver un petit runbook dans le repo ou dans le README (mais si vous savez, ces fichiers en markdown rarement mis à jour par nous autre développeurs) avec les commandes à rejouer.

Exemple de runbook minimal :

# Activer le chaos
curl -X POST http://localhost:8080/actuator/chaosmonkey/enable

# Vérifier l’état des breakers
curl http://localhost:8080/actuator/circuitbreakers

# Tester un stream
curl -X POST http://localhost:8080/api/streaming/start \\
  -H \"Content-Type: application/json\" \\
  -d '{\"userId\": 1, \"videoId\": 1}'

exemple de runbook

Tester la forteresse sous pression : charge et performance avec Gatling

Pourquoi la résilience doit résister à la montée en charge

Certaines faiblesses n’apparaissent qu’avec la charge : saturation de pools, ouverture du circuit breaker, montée des erreurs.
Le test de charge valide que les remparts tiennent quand la pression augmente.

Scénario de charge : reproduire un trafic réaliste

Le scénario Gatling (ChaosSimulation) simule des parcours d’utilisateurs réalistes :

  • navigation catalogue,
  • démarrage d’un streaming,
  • visionnage partiel,
  • mise à jour de progression.

Il alterne plusieurs profils (browser, watcher, social_watcher) pour éviter un trafic artificiel.

Extrait d’assertions définies dans la simulation :

assertions(
    global().successfulRequests().percent().gt(95.0),
    global().responseTime().percentile3().lt(1500),
    details("Watcher Journey", "Stream Video Chunk").failedRequests().percent().lt(5.0)
)

Gatling génère automatiquement un rapport HTML par exécution.
On peut donc comparer avec et sans chaos très visuellement.

Les rapports se trouvent dans :
gatling-test/target/gatling/
chaque exécution crée un dossier daté avec un index.html.

Exécution de Gatling sans avoir injecté de chaos
Exécution de Gatling après injection de chaos
public ChaosSimulation() {
    System.out.printf("[ChaosSimulation] baseUrl=%s%n", BASE_URL);

    setUp(
            userJourneyScenario.injectOpen(
                    incrementUsersPerSec(5)
                            .times(5)
                            .eachLevelLasting(Duration.ofSeconds(30))
                            .separatedByRampsLasting(Duration.ofSeconds(10))
                            .startingFrom(10)
            )
    ).protocols(HTTP_PROTOCOL)
            .assertions(
                    global().successfulRequests().percent().gt(95.0),
                    global().responseTime().percentile3().lt(1500),
                    details("Watcher Journey", "Start Streaming").responseTime().percentile3().lt(1200),
                    details("Social Watcher Journey", "Start Streaming").responseTime().percentile3().lt(1200),
                    details("Watcher Journey", "Stream Video Chunk").failedRequests().percent().lt(5.0),
                    details("Social Watcher Journey", "Stream Video Chunk").failedRequests().percent().lt(5.0)
            );
}

Lancement de la simulation Gatling

Interpréter les résultats : SLO et décisions concrètes

Une simulation Gatling ou un run Chaos Monkey n’a de valeur que si elle est reliée à des objectifs précis. Il ne s’agit pas de constater que “ça casse”, mais de mesurer où et comment la résilience tient.

Quelques indicateurs clés :

  • Taux de succès global : pourcentage de requêtes ayant renvoyé un statut attendu (200 OK par ex.).
  • Percentiles de latence (95/99) : indiquent la réactivité de l’application sous charge.
  • Taux de réponses dégradées : proportion de requêtes ayant utilisé un fallback.

En pratique, un résultat comme celui-ci :

Succès global : 81 %
95e percentile : 5,4 s
Stream Video Chunk échoué : 46 %

indique clairement que certains services subissent des saturations ou des erreurs non absorbées.

Décisions concrètes à partir de ces observations :

  1. Ajuster le circuit breaker : augmenter la fenêtre ou réduire le seuil de détection si le breaker s’ouvre trop tôt.
  2. Revoir le nombre de retries : limiter les tentatives si elles aggravent la charge, ou au contraire les augmenter si la panne est transitoire.
  3. Limiter la charge sur les endpoints critiques : utiliser bulkhead ou rate limiter pour protéger les threads et ressources internes.
  4. Optimiser la latence ou les traitements : identifier les points qui prennent trop de temps et appliquer un timeout ou une isolation.

L’idée centrale : la résilience est itérative. On observe, on ajuste, on reteste, jusqu’à atteindre un compromis acceptable entre disponibilité, latence et charge.

Voir pour croire : observabilité et signaux de résilience

Instrumenter les patterns de résilience

Les métriques Resilience4j sont exposées via Actuator :

  • GET /actuator/circuitbreakers
  • GET /actuator/retries

Ces métriques permettent de savoir quand un breaker s’ouvre, combien de retries sont déclenchés, et à quelle fréquence.

Dans le profil chaos-monkey, la santé des breakers est aussi exposée via health :

management:
  health:
    circuitbreakers:
      enabled: true

Cela donne une lecture simple : breaker ouvert = état dégradé.

{
  "circuitBreakers": {
    "recommendationService": {
      "bufferedCalls": 20,
      "failedCalls": 4,
      "failureRate": "20.0%",
      "failureRateThreshold": "40.0%",
      "notPermittedCalls": 0,
      "slowCallRate": "15.0%",
      "slowCallRateThreshold": "50.0%",
      "slowCalls": 3,
      "slowFailedCalls": 0,
      "state": "CLOSED"
    },
    "streamingService": {
      "bufferedCalls": 2,
      "failedCalls": 1,
      "failureRate": "50.0%",
      "failureRateThreshold": "40.0%",
      "notPermittedCalls": 39,
      "slowCallRate": "50.0%",
      "slowCallRateThreshold": "50.0%",
      "slowCalls": 1,
      "slowFailedCalls": 0,
      "state": "OPEN"
    }
  }
}

Un aperçu de nos circuits breakers après le test de charge

Dashboards et alertes : surveiller la forteresse en continu

Même sans stack Prometheus/Grafana, un tableau minimal (latence + état des breakers) donne une vraie lecture.
Cela permet d’identifier les dérives avant que la prod ne souffre.

Avec une stack métriques, on peut afficher :

  • ratio d’erreurs vs trafic,
  • latence moyenne vs percentile 95/99,
  • état des breakers (closed/open/half-open).

Conclusion

Construire une application “à toute épreuve” ne se résume pas à quelques annotations. Il s’agit d’une démarche globale :

  • Concevoir la résilience : identifier les zones à risque, appliquer timeout, circuit breaker, retry, fallback.
  • Tester la résilience : injecter des latences et erreurs via Chaos Monkey, simuler la charge via Gatling.
  • Mesurer et surveiller : suivre les metrics Resilience4j, les taux de succès, les percentiles de latence et les réponses dégradées.

Avec cette approche, une application peut continuer à servir un système acceptable, même sous pression, et offrir à l’utilisateur une expérience cohérente, dégradée mais compréhensible. La résilience devient alors un processus mesurable et répétable, et non une simple réaction aux incidents.

Résilience applicative avec Resilience4j et Spring Boot
Découvrez comment rendre vos applications Spring Boot plus résilientes avec Resilience4j : circuit breaker, retry, rate limiter, bulkhead et time limiter, expliqués clairement avec des exemples concrets et des bonnes pratiques pour tolérer les pannes.
Introduisez du chaos dans votre application Spring Boot
Le chaos n’est pas une menace, mais un révélateur : il expose les failles et forge la résilience. Découvrez comment Chaos Monkey met votre application Spring Boot à l’épreuve pour la rendre plus robuste que jamais.

Dernier