Clés d’Idempotence dans les APIs Distribuées : Éviter les Doublons de Transactions
Mettre en place des clés d’idempotence avec verrous Redis, contraintes PostgreSQL et rejeu de réponses.
1. Pourquoi les retries réseau exigent l’idempotence
Chaque bibliothèque client HTTP qui implémente des retries — les intercepteurs Axios, la politique de retry intégrée du SDK AWS, le budget de retry d’un service mesh — fait un pari : que renvoyer une requête identique est sûr si la réponse de la première tentative n’a jamais été reçue. Ce pari n’est vrai que si l’opération côté serveur est idempotente. Pour un GET, c’est gratuit ; un GET n’a par contrat aucun effet de bord. Pour un POST qui débite une carte bancaire ou un portefeuille, ce n’est absolument pas gratuit, et un retry naïf après un timeout peut facturer le client deux fois pour un seul achat.
Le mode de défaillance qui rend cela inévitable, et pas seulement théorique, est le timeout ambigu : votre client envoie un POST, le serveur le traite avec succès et valide la charge, mais le paquet de réponse est perdu, ou la connexion tombe avant que le client ne la lise. Du point de vue du client, c’est indistinguable d’une requête n’ayant jamais atteint le serveur. Un client qui traite tout timeout comme définitivement pas arrivé et retente aveuglément facturera deux fois dans exactement ce scénario, et ce scénario est assez fréquent sur des réseaux réels — un drain de connexions de load balancer pendant un déploiement, une brève partition réseau, un timeout client réglé trop agressivement — pour être considéré comme routinier, pas rare.
La solution n’est pas d’arrêter de retenter — les retries sont essentiels pour la résilience face aux défaillances transitoires — mais de rendre l’opération elle-même sûre à répéter. La clé d’idempotence est ce mécanisme : le client génère un jeton unique par opération logique (pas par tentative HTTP) et le serveur l’utilise pour reconnaître un retry de la même intention métier comme distinct d’une nouvelle requête, afin de renvoyer le résultat original plutôt que d’exécuter à nouveau l’effet de bord.
2. Le workflow de clé d’idempotence
Le client génère un UUID (v4 convient ; la seule exigence est une unicité effective par opération logique) une seule fois, avant la première tentative, et l’envoie dans un header `Idempotency-Key` à chaque retry de cette même opération — jamais une nouvelle clé par retry, ce qui annulerait tout le mécanisme. Cette clé doit être générée au moment où l’intention métier se forme (l’instant où l’utilisateur clique sur Payer), pas régénérée par la logique de retry elle-même.
Côté serveur, la première chose que fait le handler — avant de toucher à la moindre logique métier — est de tenter de réclamer la clé de façon atomique : `INSERT INTO idempotency_keys (key, status) VALUES ($1, 'processing') ON CONFLICT (key) DO NOTHING RETURNING *`. Si l’insertion réussit, il s’agit réellement d’une nouvelle opération et le traitement se poursuit. Si elle entre en conflit, une requête avec cette clé a déjà été vue, et le handler consulte son résultat enregistré au lieu de ré-exécuter quoi que ce soit.

