Idempotence

Comment concevoir des opérations qui peuvent être exécutées plusieurs fois sans effets secondaires dupliqués, un élément essentiel pour des tentatives sûres et un traitement fiable des événements.

Quel problème cela résout

Dans les systèmes distribués, les messages et les requêtes peuvent arriver plus d’une fois pour diverses raisons :

  • Le client réessaie une requête parce qu’il n’a pas reçu de réponse (alors que le serveur l’a bien traitée)
  • Le broker de messagerie livre un événement dupliqué (garantie at-least-once)
  • Une panne réseau provoque la perte de la confirmation, et l’émetteur renvoie la requête
  • L’utilisateur double-clique sur le bouton « Payer »
Sans idempotence :
  Client ──► POST /payments {amount: 100}
              ✅ Paiement traité (mais la réponse se perd)
  
  Client ──► POST /payments {amount: 100}  (nouvelle tentative)
              ✅ Paiement traité UNE NOUVELLE FOIS
  
  Résultat : 200 $ ont été facturés au lieu de 100 $

Sans idempotence, les tentatives et les doublons provoquent des effets secondaires répétés : doubles facturations, stock décompté deux fois, e-mails dupliqués, enregistrements dupliqués dans la base de données.

Comment ça fonctionne

Une opération est idempotente lorsque l’exécuter une fois produit le même résultat que l’exécuter plusieurs fois. Le système détecte les requêtes dupliquées et renvoie le résultat original sans réexécuter l’opération.

Avec idempotence :
  Client ──► POST /payments
              Idempotency-Key: "PAY-abc-123"
              {amount: 100}
              ✅ Paiement traité → la réponse se perd
  
  Client ──► POST /payments
              Idempotency-Key: "PAY-abc-123"  (même clé)
              {amount: 100}
              ✅ Renvoie le résultat précédent (sans retraiter)
  
  Résultat : 100 $ facturés une seule fois

Mécanisme avec Idempotency Key

L’approche la plus courante consiste à utiliser une clé d’idempotence (idempotency key) qui identifie de manière unique chaque opération :

1. Le client génère un ID unique : "PAY-abc-123"
2. Le client envoie la requête avec l'en-tête Idempotency-Key
3. Le serveur vérifie s'il a déjà traité cette clé :
   a. Si elle N'existe PAS → traite l'opération, enregistre le résultat avec la clé
   b. Si elle existe DÉJÀ → renvoie le résultat enregistré sans retraiter

Stockage des clés

StockageAvantagesInconvénients
Base de donnéesPersistant, transactionnelPlus lent, nécessite un nettoyage
Redis/CacheRapide, TTL automatiquePeut perdre des données en cas de redémarrage
Table dédiéeSéparation claire, facile à consulterTable supplémentaire à maintenir

Exemple de flux complet

Table : idempotency_keys
┌──────────────────┬────────┬──────────────────┬─────────┐
│ key              │ status │ response         │ expires │
├──────────────────┼────────┼──────────────────┼─────────┤
│ PAY-abc-123      │ done   │ {paymentId: 456} │ 24h     │
│ PAY-def-456      │ doing  │ null             │ 24h     │
└──────────────────┴────────┴──────────────────┴─────────┘

Requête 1 : POST /payments (key: PAY-abc-123)
  → La clé n'existe pas → INSERT de la clé avec status "doing"
  → Traiter le paiement → UPDATE de la clé avec status "done" et response
  → Renvoyer la response

Requête 2 : POST /payments (key: PAY-abc-123) [nouvelle tentative]
  → La clé existe avec status "done"
  → Renvoyer la response enregistrée (sans retraiter)

Requête 3 : POST /payments (key: PAY-def-456)
  → La clé existe avec status "doing" (en cours de traitement)
  → Renvoyer HTTP 409 Conflict (patientez et réessayez)

Idempotence chez les consommateurs d’événements

Pour les consommateurs d’événements, l’idempotence s’implémente en suivant quels événements ont déjà été traités :

Événement reçu : OrderCreated (eventId: "EVT-789")

1. Vérifier si eventId a déjà été traité
   → SELECT * FROM processed_events WHERE event_id = 'EVT-789'
   
2a. S'il N'existe PAS :
   → Traiter l'événement (réserver le stock)
   → INSERT INTO processed_events (event_id, processed_at)
   → ACK au broker
   
2b. S'il existe DÉJÀ :
   → Ignorer (déjà traité)
   → ACK au broker

Opérations naturellement idempotentes

Certaines opérations sont idempotentes par nature et n’ont pas besoin de mécanismes supplémentaires :

✅ Naturellement idempotentes :
  PUT /users/123 {name: "María"}     → Laisse toujours le même état
  DELETE /orders/456                   → Supprimer une chose déjà supprimée = no-op
  GET /products/789                    → Lecture, sans effets secondaires

❌ NON idempotentes (nécessitent un mécanisme) :
  POST /payments {amount: 100}         → Chaque exécution crée un nouveau paiement
  POST /orders {items: [...]}          → Chaque exécution crée une nouvelle commande
  POST /notifications {msg: "Hola"}    → Chaque exécution envoie un message

Avantages

  • Tentatives sûres : Les clients peuvent réessayer sans craindre d’effets dupliqués
  • Traitement fiable des événements : Les consommateurs gèrent correctement les doublons
  • Expérience utilisateur : Le double-clic ne cause aucun problème
  • Cohérence des données : Aucun enregistrement dupliqué ni double facturation
  • Simplifie la résilience : Le pattern Retry fonctionne correctement lorsque le service en aval est idempotent
  • Audit : Le registre des idempotency keys sert de journal des opérations

Compromis / Inconvénients

  • Stockage supplémentaire : Vous devez enregistrer les clés et leurs résultats
  • Complexité d’implémentation : Gérer les états concurrents (doing/done) demande de la prudence
  • Nettoyage des clés : Les anciennes clés doivent être nettoyées périodiquement (TTL)
  • Génération des clés : Le client doit générer des clés uniques et cohérentes
  • Latence : La vérification des doublons ajoute une requête supplémentaire par opération
  • Cohérence transactionnelle : La clé et l’opération doivent être enregistrées dans la même transaction

Quand l’utiliser

  • Opérations de paiement et transactions financières
  • Tout endpoint susceptible de recevoir des tentatives (POST, PATCH avec effets secondaires)
  • Consommateurs d’événements dans des architectures event-driven
  • Opérations déclenchées par des webhooks (qui peuvent arriver en double)
  • Toute opération où un doublon nuit à l’activité
  • APIs publiques où vous ne contrôlez pas le comportement du client

Quand l’éviter

  • Opérations en lecture seule (GET) qui sont déjà idempotentes
  • Opérations PUT/DELETE qui sont naturellement idempotentes
  • Systèmes internes avec une communication fiable et sans tentatives
  • Prototypes où la complexité supplémentaire ne se justifie pas

Technologies et implémentations courantes

CatégorieOptions
Stockage des clésRedis (avec TTL), PostgreSQL, DynamoDB
En-têtes standardsIdempotency-Key (Stripe), X-Request-Id, If-Match (ETags)
BibliothèquesStripe SDK (idempotence intégrée), middleware personnalisé
Brokers avec déduplicationKafka (exactly-once semantics), SQS (deduplication ID)
FrameworksMiddleware Express, interceptor Spring, filter ASP.NET

Relation avec d’autres patterns

  • Retry : Les tentatives ne sont sûres que si le service en aval est idempotent
  • Event-Driven Architecture : Les consommateurs d’événements doivent être idempotents (at-least-once delivery)
  • Outbox Pattern : Garantit la publication des événements, mais le consommateur a tout de même besoin d’idempotence
  • Saga Pattern : Chaque étape de la saga doit être idempotente pour gérer les tentatives de compensation
  • Circuit Breaker : Lorsque le circuit se referme et que les opérations sont réessayées, l’idempotence évite les doublons

Prochaines étapes

L’idempotence est une exigence fondamentale pour des systèmes distribués fiables. Pour comprendre comment partager des modèles de manière contrôlée entre services, explorez le Shared Kernel. Pour voir comment l’idempotence s’applique aux transactions distribuées, consultez le Saga Pattern.