Domaines et cycles de vie - 08 - Idempotence : exécuter sans crainte

L'idempotence expliquee simplement : pourquoi c'est indispensable pour les API, les transitions d'état et les systèmes distribues.

08 - Idempotence : exécuter sans crainte

Ce que tu vas apprendre

  • Ce qu'est l'idempotence et pourquoi c'est essentiel
  • Pourquoi ca compte pour les transitions d'état
  • Des exemples concrets dans le monde réel
  • Comment concevoir des transitions idempotentes

Prerequisites


Définition

Une opération est idempotente si l'exécuter plusieurs fois produit exactement le meme résultat qu'une seule exécution.

Exemples du quotidien :

  • Appuyer sur le bouton "etage 3" dans un ascenseur : que tu appuies une fois ou dix fois, tu vas au 3e etage. C'est idempotent.
  • Poser un verre sur une table : la première fois il passe de "en main" a "sur la table". La deuxieme fois, il est deja sur la table. Rien ne change. C'est idempotent.
  • Ajouter 10 euros sur un compte : chaque exécution ajoute 10 euros. Deux executions = 20 euros. Ce n'est PAS idempotent.

En termes de code :

typescript// Idempotent : le resultat est toujours le meme
function setStatus(place: Place, newStatus: string): Place {
  return { ...place, status: newStatus };
}
// setStatus(place, "ENRICHED") appele 1 fois ou 5 fois = meme resultat

// PAS idempotent : le resultat change a chaque appel
function incrementViews(place: Place): Place {
  return { ...place, views: place.views + 1 };
}
// incrementViews(place) appele 5 fois = views augmente de 5

Pourquoi ca compte pour les transitions

Dans un système distribue (plusieurs serveurs, des workers, des cron jobs, des requêtes qui timeout et sont retentees), une meme opération peut etre exécutée plus d'une fois. C'est inevitable.

Si ta transition n'est pas idempotente, le retry peut créer des problèmes :

typescript// Transition NON idempotente : danger
async function publishPlace(placeId: string) {
  const place = await db.findById(placeId);
  place.status = "PUBLISHED";
  place.publishedAt = new Date(); // Change a chaque appel
  place.publishCount += 1; // Incremente a chaque appel !
  await db.save(place);
  await emailService.send(`Place ${place.name} publiee`); // Email envoye a chaque retry
}

Si cette fonction est appelee deux fois (timeout + retry), on a : publishCount a 2 au lieu de 1, deux emails envoyes, et un publishedAt différent.

Concevoir des transitions idempotentes

Regle 1 : vérifier l'état avant de transitionner

Si l'entité est deja dans l'état cible, c'est un no-op (no opération), pas une erreur.

typescriptasync function publishPlace(placeId: string) {
  const place = await db.findById(placeId);

  // Deja publiee ? On ne fait rien. Pas d'erreur.
  if (place.status === "PUBLISHED") {
    return place; // no-op
  }

  // Verifier que la transition est autorisee
  if (place.status !== "READY_FOR_PUBLICATION") {
    throw new Error(`Impossible de publier depuis l'etat ${place.status}`);
  }

  return transition(place, "PUBLISH");
}

Regle 2 : utiliser des identifiants pour éviter les doublons

Pour les side effects (emails, paiements, etc.), utilise un identifiant d'idempotence :

typescriptasync function sendPublicationEmail(place: Place, transitionId: string) {
  // Verifier si on a deja envoye cet email pour cette transition
  const alreadySent = await emailLog.exists(transitionId);
  if (alreadySent) return; // no-op

  await emailService.send(`Place ${place.name} publiee`);
  await emailLog.record(transitionId);
}

Regle 3 : utiliser des ecritures "set", pas "increment"

typescript// Mauvais : pas idempotent
await db.query("UPDATE places SET publish_count = publish_count + 1 WHERE id = $1", [placeId]);

// Bon : idempotent
await db.query("UPDATE places SET status = 'PUBLISHED', published_at = $2 WHERE id = $1", [placeId, now]);

Exemples dans le monde réel

Paiement

Les systèmes de paiement (Stripe, PayPal) utilisent une clé d'idempotence. Tu envoies un paiement avec une clé unique. Si le meme paiement est envoye deux fois avec la meme clé, le deuxieme est ignore.

typescriptawait stripe.charges.create(
  { amount: 1000, currency: "eur" },
  { idempotencyKey: `order_${orderId}_payment` }
);

API REST

La méthode HTTP PUT est idempotente par design : "mets cette ressource dans cet état". Que tu envoies le PUT une fois ou dix fois, le résultat est le meme. La méthode POST ne l'est pas : chaque POST peut créer une nouvelle ressource.

Batch opérations

Si un batch de 1000 Places plante a la Place 500, tu dois pouvoir le relancer. Si les transitions sont idempotentes, les 500 premières Places seront des no-ops (deja traitees), et le batch reprend a la 501e.

typescriptfor (const place of places) {
  // Si deja traitee, skip. Si pas traitee, transition.
  if (place.status === targetStatus) continue;
  await transition(place, event);
}

Le lien avec les transitions d'état

Dans une state machine bien conçue, l'idempotence est naturelle :

  • Si place.status === "ENRICHED" et qu'on tente ENRICH, c'est un no-op
  • La transition ne s'exécuté que si l'état source correspond
  • Les guards empechent les transitions invalides

Le cycle de vie protégé l'idempotence : il est impossible de "re-publier" une Place deja publiee, parce que la transition PUBLISH n'est autorisee que depuis READY_FOR_PUBLICATION.

C'est le genre de bonnes pratiques qu'on retrouve dans tout projet backend bien structure, et qu'on applique aussi sur paltemps.fr pour garantir la fiabilité de l'automatisation.


Résumé

  • Une opération idempotente produit le meme résultat qu'elle soit exécutée une fois ou cent fois
  • C'est essentiel dans les systèmes distribues ou les retries sont inevitables
  • Trois regles : vérifier l'état avant, utiliser des identifiants d'idempotence, préférer "set" a "increment"
  • Une state machine bien conçue rend les transitions naturellement idempotentes
  • L'idempotence rend les batch opérations et les retries surs

Article précédent : 07 - Les invariants Article suivant : 09 - La dette technique : quand on construit sans lifecycle

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.