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
| Stockage | Avantages | Inconvénients |
|---|---|---|
| Base de données | Persistant, transactionnel | Plus lent, nécessite un nettoyage |
| Redis/Cache | Rapide, TTL automatique | Peut perdre des données en cas de redémarrage |
| Table dédiée | Séparation claire, facile à consulter | Table 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égorie | Options |
|---|---|
| Stockage des clés | Redis (avec TTL), PostgreSQL, DynamoDB |
| En-têtes standards | Idempotency-Key (Stripe), X-Request-Id, If-Match (ETags) |
| Bibliothèques | Stripe SDK (idempotence intégrée), middleware personnalisé |
| Brokers avec déduplication | Kafka (exactly-once semantics), SQS (deduplication ID) |
| Frameworks | Middleware 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.