Développement web

Outbox transactionnelle : retrouver chaque intention après une panne

Jérémy Chomel Dawap
  • Publié le : 25 octobre 2026
  • Mis à jour le : 27 octobre 2026
  • Temps de lecture : 19 minutes
  1. Dans quel cas une outbox répond au bon problème
  2. Engager la commande et son intention dans la même transaction
  3. Lire les fenêtres de panne avant d’écrire le relais
  4. Réserver le travail sans laisser un ancien worker le terminer
  5. Rendre l’effet local du consommateur idempotent
  6. Exécuter l’arrêt après commit et la réponse perdue
  7. Brancher le mécanisme dans Symfony sans surpromettre
  8. Choisir un relais SQL ou une capture de changements
  9. Repérer le retard que les métriques de débit cachent
  10. Erreurs fréquentes qui annulent la protection attendue
  11. Plan d’action pour réceptionner une outbox récupérable
  12. Guides complémentaires pour éprouver les traitements différés
  13. Conclusion : conserver l’intention et maîtriser sa répétition
Portrait de Jérémy Chomel

Une commande est enregistrée, la réponse HTTP confirme son numéro, mais rien ne part vers la préparation. Le processus s’est arrêté entre le commit SQL et l’envoi du message. Relancer le serveur ne suffit pas : aucune intention durable ne dit au système qu’il reste quelque chose à publier. La panne laisse une commande parfaitement visible dans la base et absente du circuit qui doit la traiter. Le client attend sa préparation, tandis que le support cherche la cause de ce retard.

En pratique, une outbox transactionnelle doit conserver l’intention avec la donnée métier, puis accepter que sa transmission puisse être répétée. La reprise se joue ensuite à deux endroits : le relais doit retrouver le travail abandonné, et le destinataire doit reconnaître une livraison déjà appliquée. Le mot « transactionnel » ne couvre pas automatiquement ces deux frontières.

Le scénario fictif ci-dessous suit une commande C42 et son événement E42. Un programme téléchargeable provoque un véritable arrêt de processus après commit, simule une réponse perdue et vérifie la reprise d’une réservation expirée. Il utilise SQLite localement, sans broker ni API externe : les conclusions portent sur les invariants exécutés, pas sur une certification de Symfony ou PostgreSQL.

Pour un projet de développement d’application métier sur mesure, cette frontière conditionne la confiance dans les confirmations affichées. Un middleware conçu pour vos échanges doit rendre les intentions en attente consultables et récupérables, avec un propriétaire et une preuve de traitement, même lorsque le réseau ne permet plus de connaître immédiatement le résultat.

Dans quel cas une outbox répond au bon problème

Identifier les deux écritures qui peuvent diverger

Le besoin apparaît lorsqu’une même décision doit modifier une base locale et déclencher une opération dans un autre système. Par exemple, valider une commande puis prévenir le WMS, accepter un dossier puis lancer sa vérification, ou enregistrer une modification puis actualiser une projection. Les deux écritures n’appartiennent généralement pas à la même transaction locale.

Commencez par nommer ce qui doit être engagé ensemble. Si la commande peut exister sans préparation immédiate, son état peut devenir « enregistrée, transmission en attente ». Si le métier exige une réservation externe préalable, l’outbox ne supprime pas cette dépendance : il faut modéliser une étape provisoire et son issue avant d’annoncer une disponibilité garantie.

Écarter les usages qui ne nécessitent pas cette mécanique

Une lecture synchrone sans effet durable n’a pas besoin d’une outbox. Une notification purement informative, reconstruisible depuis un état fiable, peut accepter une politique plus simple si sa perte est assumée. À l’inverse, une instruction de débit ou de réservation ne doit pas être rangée dans cette catégorie par commodité technique.

Comparez le coût de l’exploitation au coût d’une intention oubliée : stockage, supervision, rétention, reprise et gestion des versions font partie de la solution. La décision doit couvrir le service rendu après la panne, pas seulement la facilité d’ajouter une table dans une migration Doctrine.

Engager la commande et son intention dans la même transaction

Donner une identité durable à l’événement

Notre commande C42 porte un montant fictif de 12 000 centimes. Sa validation insère également E42 dans l’outbox avec l’état « pending ». Ces deux écritures utilisent la même connexion et la même transaction. Si une exception survient avant commit, un observateur indépendant ne doit voir ni commande ni événement. Après commit, il doit retrouver les deux.

E42 identifie cette intention précise. Ce numéro reste stable pendant les reprises, alors que chaque tentative reçoit sa propre référence. Réutiliser le seul numéro de commande pour tous ses événements confondrait création, annulation et correction. Générer un nouvel identifiant d’événement à chaque envoi empêcherait le destinataire de reconnaître une redélivrance.

Conserver un contrat exploitable après un déploiement

Une implémentation complète ajoute le type d’événement, sa version de schéma, l’agrégat concerné et les données nécessaires au destinataire. Le payload doit représenter l’intention convenue ; une simple référence vers une ligne mutable peut changer de sens entre l’enregistrement et la lecture. Décidez explicitement si le consommateur attend un fait historique ou l’état courant.

La démonstration conserve seulement l’identité de commande, car elle mesure un effet local abstrait. Elle ne sérialise pas une commande réelle. Dans une application PHP, testez aussi la compatibilité entre un ancien événement encore en attente et le nouveau code du consommateur. Un déploiement réussi ne justifie pas de supprimer les messages devenus illisibles.

Lire les fenêtres de panne avant d’écrire le relais

Placer la preuve au bon endroit

La question utile est : « Quel état durable reste visible si le processus s’arrête ici ? » Avant le commit, aucune promesse n’est acquise. Après le commit, une intention doit être retrouvable. Après l’envoi mais avant l’enregistrement de son résultat, le relais peut ignorer que le destinataire a déjà appliqué l’opération. Cette dernière fenêtre impose la tolérance aux doublons.

InterruptionÉtat attenduReprise
Avant commit localNi C42 ni E42Reprendre la demande selon sa clé métier
Après commit, avant envoiC42 et E42 en attenteRéserver E42 puis transmettre
Après effet distant, réponse perdueEffet possible, E42 non acquitté localementRenvoyer E42 avec déduplication côté réception
Après acquittement durableE42 transmisNe plus sélectionner E42 comme travail courant

Refuser le marquage anticipé comme raccourci

Marquer « envoyé » avant de publier évite certains doublons visibles, mais réintroduit une perte silencieuse si le processus tombe avant le réseau. Marquer après publication permet la reprise, au prix d’une possible répétition. Le risque est de croire que le premier choix est plus fiable parce que les compteurs de doublons diminuent.

Les deux ordres ne constituent donc pas deux variantes équivalentes. Le choix doit préserver une intention récupérable et faire porter la protection contre les répétitions à l’endroit où l’effet est réellement appliqué. Un tableau de bord vert n’est pas une preuve si les commandes oubliées ont simplement quitté la sélection.

Réserver le travail sans laisser un ancien worker le terminer

Utiliser un bail et une référence de tentative

Dans la recette, le worker A réserve E42 à l’instant fictif 100 pour dix unités de temps. À 105, B ne peut pas prendre la même entrée. À 111, le bail a expiré : B peut la reprendre avec son propre jeton. Ces nombres servent à vérifier une frontière ; ils ne recommandent pas un délai universel de dix secondes.

Le marquage final vérifie l’identité de l’événement, son état et le jeton de la réservation courante. Un retour tardif de A ne peut alors pas terminer la tentative de B. L’expiration seule ne suffit pas : sans cette comparaison, un ancien worker pourrait écraser un résultat plus récent après avoir retrouvé le réseau.

Ne pas confondre exclusion locale et arrêt du monde extérieur

Un bail expiré ne tue pas le worker précédent et n’annule pas son appel externe. A et B peuvent encore transmettre E42. Le jeton protège la transition dans l’outbox, tandis que l’identité stable E42 protège l’effet côté consommateur. Supprimer l’un de ces contrôles parce que l’autre existe ouvre une fenêtre différente.

Le producteur doit utiliser une horloge cohérente pour décider l’expiration, borner la durée de ses appels et renouveler prudemment les traitements longs. Sur une architecture distribuée, une réservation peut être perdue pendant une pause du processus. La reprise doit rester correcte même lorsque l’ancien worker reprend son exécution après cette pause.

Rendre l’effet local du consommateur idempotent

Enregistrer la réception et l’effet ensemble

La base du consommateur possède une inbox dont la clé unique est E42. Dans la même transaction, elle inscrit cette clé puis incrémente le compteur de préparation simulé. Si E42 est déjà présent, l’effet n’est pas appliqué une deuxième fois. Si une erreur intervient avant commit, la clé et l’effet sont tous deux annulés.

La recette provoque cette erreur avant de retenter. Une inbox écrite séparément de l’effet pourrait bloquer définitivement une opération jamais réalisée ; une inbox écrite après un effet non transactionnel pourrait laisser ce dernier se répéter. La preuve recherchée porte donc sur leur atomicité locale, pas uniquement sur l’existence d’une contrainte unique.

Reconnaître la nouvelle frontière lorsqu’un autre service intervient

Le compteur de la démonstration appartient à la même base que l’inbox. Un envoi d’email, un paiement ou une réservation dans un WMS ne partage pas cette transaction. Pour ces effets, il faut une clé d’idempotence reconnue par le fournisseur, une nouvelle intention durable, ou une procédure de rapprochement lorsque le résultat reste inconnu.

Le dossier sur l’idempotence et la reprise après erreur détaille cette distinction. La présence d’une inbox dans votre application ne démontre pas qu’un système tiers ne prélèvera jamais deux fois. La garantie doit suivre la chaîne jusqu’à l’opération que le métier cherche à protéger.

Exécuter l’arrêt après commit et la réponse perdue

Observer une base depuis un nouveau processus

Le programme de recette de l’outbox s’exécute avec Python et sa bibliothèque standard. Il crée deux bases temporaires, lance un processus enfant qui écrit puis quitte avec le code 81 après commit, et rouvre la base depuis le parent. C42 et E42 doivent alors être présents, sans avoir besoin de reconstruire l’intention à partir des logs.

Le programme simule ensuite une livraison appliquée dont la réponse n’est pas enregistrée par le producteur. B reprend la réservation expirée et livre à nouveau E42. Le compteur reste à un, l’inbox contient une seule entrée et le nombre de tentatives atteint deux. Un témoin volontairement naïf, sans déduplication, applique deux effets pour les deux livraisons.

python3 outbox-reprise-pannes.py
# Arrêt réel du processus enfant après commit : code 81.
# Deux tentatives de livraison ; un effet local protégé.
# Acquittement de l’ancien worker refusé.
# Témoin sans inbox : deux effets.

Délimiter précisément ce qui a été éprouvé

Les assertions couvrent le rollback producteur, la persistance après arrêt, l’exclusion avant expiration, la reprise du bail, le refus du jeton périmé et le rollback consommateur. Les entrelacements sont déterministes. Ce test n’exécute pas deux workers réellement concurrents et ne mesure ni débit, ni durabilité matérielle, ni comportement d’un broker.

Pour réceptionner votre installation, ajoutez les coupures de connexion, redémarrages de conteneurs, verrous concurrents et limites de stockage sur les composants retenus. Conservez un observateur indépendant de la transaction testée. Une assertion effectuée uniquement dans la connexion qui vient d’écrire peut manquer une différence entre données visibles localement et données réellement engagées.

Brancher le mécanisme dans Symfony sans surpromettre

Vérifier la connexion qui porte les écritures

La documentation Messenger sur Doctrine, vérifiée le 25 octobre 2026, décrit le middleware transactionnel et le rollback des handlers concernés. Cela ne rend pas atomique un envoi vers un broker externe avec votre commit SQL. La configuration effective du bus et du transport doit donc être examinée, pas seulement le nom du middleware.

Dans notre architecture proposée, le handler métier écrit sa donnée et l’outbox sur la même connexion. Un service distinct assure la publication. L’entrée du relais est une intention persistée ; sa sortie est une transmission confirmée selon le contrat du transport ou une attente attribuée. La journalisation conserve événement, tentative et erreur, sans confondre publication et exécution distante.

Garder la reprise visible dans l’interface

Le backend doit exposer « transmission en attente » lorsque cette nuance change l’action de l’utilisateur. Un bouton de relance ne crée pas une nouvelle commande : il demande une reprise contrôlée de l’intention existante. Le support doit pouvoir voir l’ancienneté, le prochain essai et le motif de blocage sans éditer directement une ligne SQL.

La responsabilité du relais couvre réservation, repli et traçabilité. Celle du consommateur couvre l’idempotence de son effet. Le contrat d’exploitation précise qui prend une exception en charge et comment la clôturer avec une preuve. L’article sur Messenger dans l’exploitation d’une application Symfony aide à relier ces états techniques à une promesse utilisateur compréhensible.

Choisir un relais SQL ou une capture de changements

Partager une file SQL sans prétendre préserver tout ordre

PostgreSQL documente SKIP LOCKED pour des usages de type file : les lignes déjà verrouillées sont ignorées. Ce comportement réduit certaines attentes entre consommateurs, mais donne une vue incomplète et ne convient pas à une lecture générale censée représenter toutes les données. La recette SQLite ne teste pas cette primitive PostgreSQL.

Avec plusieurs relais, la réservation de lignes distinctes ne garantit pas l’ordre de publication des événements d’une même commande. Si cet ordre compte, prévoyez une séquence par agrégat et un traitement des trous, ou une stratégie de partitionnement adaptée. Une clause de tri dans la sélection ne suffit pas à ordonner les appels réseau exécutés ensuite par différents workers.

Évaluer le CDC avec ses propres responsabilités

Le routeur outbox de Debezium exploite les changements de la table et distingue notamment l’identifiant d’événement de la clé d’agrégat. Cette dernière joue un rôle dans la répartition des messages Kafka. Le choix peut être pertinent lorsqu’une infrastructure de capture est déjà exploitée avec des garanties et une supervision connues.

Il ne faut pas additionner sans analyse un relais qui modifie les états et une capture conçue pour des événements immuables. Définissez quel composant publie, quel format il attend, comment il reprend et comment la rétention est pilotée. L’économie de code applicatif doit être comparée à l’exploitation du connecteur, des positions de lecture et des éventuels journaux accumulés.

Repérer le retard que les métriques de débit cachent

Surveiller la plus ancienne intention encore active

Un débit moyen satisfaisant peut masquer une poignée de commandes bloquées depuis longtemps. Premier signal faible : l’âge maximal progresse alors que le volume total diminue. Deuxième signal : les reprises de baux expirés augmentent sans hausse comparable des nouveaux événements. Ces observations orientent vers une famine de certaines lignes ou des workers incapables de terminer à temps.

Mesurez séparément attente de publication, nombre de tentatives et confirmation métier distante lorsqu’elle existe. Fixez un seuil en fonction de la prochaine échéance opérationnelle. Une préparation avant départ transport et une actualisation de tableau de bord n’ont pas le même budget de retard ; un unique seuil technique peut rester vert alors que la commande devient inutilisable.

Dimensionner aussi le rattrapage

Par exemple, une arrivée hypothétique de 50 événements par seconde crée 180 000 intentions pendant une heure d’arrêt. Si le relais reprend à 80 par seconde pendant que l’arrivée reste à 50, la capacité nette de rattrapage est de 30 par seconde. Il faut alors 6 000 secondes, soit 100 minutes, pour absorber ce retard dans ces conditions constantes.

Ce calcul ne représente pas un benchmark. Il rend visible un coût souvent oublié : revenir en ligne ne signifie pas revenir à jour. La capacité du destinataire, les limites API et la taille des messages peuvent réduire ce débit. Réservez une marge et donnez à l’exploitation un indicateur de délai estimé, assorti de ses hypothèses.

Erreurs fréquentes qui annulent la protection attendue

Faire disparaître l’intention pour nettoyer le tableau de bord

Une suppression automatique des entrées anciennes peut effacer précisément les dossiers qui demandent une intervention. Séparez événements terminés, tentatives échouées et intentions encore actives. La rétention doit conserver la preuve nécessaire au rapprochement et les identifiants de déduplication pendant la fenêtre où un rejeu reste possible.

  • Écrire l’outbox avec une autre connexion hors de la transaction métier.
  • Changer l’identifiant d’événement lors de chaque retry.
  • Marquer « envoyé » avant d’obtenir la confirmation attendue du transport.
  • Autoriser un jeton de réservation périmé à clore une tentative récente.
  • Purger l’inbox alors que des événements anciens peuvent encore revenir.

Rejouer sans connaître la portée du résultat

Une réponse perdue demande de déterminer ce qui a pu être appliqué. Pour un consommateur idempotent éprouvé, la redélivrance peut être prévue par contrat. Pour une API qui ne propose ni clé stable ni consultation fiable, la même action peut exiger un rapprochement humain. L’outbox garde le problème visible ; elle ne fabrique pas une garantie absente chez le destinataire.

Consignez la procédure dans le runbook avec les références à vérifier, le responsable et l’action interdite tant que l’incertitude subsiste. Un bouton « tout rejouer » ne doit pas mélanger événements sûrs à répéter et opérations susceptibles de produire un nouvel effet commercial.

Plan d’action pour réceptionner une outbox récupérable

Construire la preuve sur une seule décision métier

Choisissez une commande représentative, son événement et le destinataire concerné. Décrivez les états visibles après chaque interruption avant de sélectionner un transport. Vérifiez la transaction réelle, la connexion utilisée et la stabilité de l’identité d’événement. Faites approuver la différence entre commande enregistrée, message transmis et opération réalisée par les équipes qui répondront au client.

Exécutez ensuite les pannes dans un environnement de test : avant commit, après commit, après application distante et avant acquittement. Ajoutez une reprise de bail et un ancien worker qui revient. Chaque test doit observer un état durable depuis une nouvelle session, puis comparer le nombre d’effets au nombre d’intentions distinctes. Un simple message de succès dans les logs n’est pas un critère de réception.

Ouvrir le relais avec une capacité de reprise mesurée

Limitez le premier périmètre et préparez les alertes avant la bascule. Mesurez le retard initial, le débit soutenable du destinataire et la capacité nette de rattrapage. Conservez les événements en attente lors du déploiement et testez leur décodage par le nouveau consommateur. Le responsable d’exploitation doit savoir interrompre les nouvelles publications sans supprimer le travail à reprendre.

Avant une intervention, le passage d’une application en lecture seule doit également couvrir ce relais et ses consommateurs. Le bandeau de maintenance ne suspend pas les intentions déjà persistées.

La décision d’étendre repose sur les exceptions réellement observées et sur leur résolution. Vérifiez notamment les reprises manuelles, la rétention de l’inbox et le comportement des messages trop anciens. Refusez l’élargissement si un arrêt banal laisse des intentions invisibles ou si un rejeu peut doubler un effet sans alerte. La qualité se mesure dans la récupération démontrée, au-delà du fonctionnement nominal.

Guides complémentaires pour éprouver les traitements différés

La cohérence de l’outbox ne dispense pas de vérifier la qualité des tests et les règles de lecture des messages conservés. Deux approfondissements aident à préparer les déploiements sans confondre ces responsabilités.

Construire un résultat attendu indépendant du code testé

La méthode des tests avec un oracle indépendant aide à choisir une preuve qui ne répète pas l’algorithme. Pour l’outbox, observer une nouvelle connexion et compter les effets réels évite de valider uniquement les valeurs internes du relais.

Conservez aussi un témoin incorrect qui doit échouer à votre critère. Ici, deux livraisons sans inbox produisent deux effets : cette différence montre que l’assertion sait distinguer une reprise protégée d’une simple répétition.

Faire survivre les messages aux changements de secrets

La rotation des secrets de signature dans Messenger traite la lecture des messages déjà présents pendant une évolution de configuration. Elle complète la recette lorsque le transport utilisé signe ses enveloppes et conserve une file ancienne.

Une intention durable mais impossible à décoder ou à vérifier reste bloquée. Préparez donc la compatibilité de lecture avant de changer les producteurs, puis exercez la reprise sur des messages créés avant le déploiement.

Conclusion : conserver l’intention et maîtriser sa répétition

Une outbox utile relie la décision métier à une intention durable. Elle ferme la perte entre commit local et publication, à condition que les deux écritures partagent réellement la transaction. Elle ne supprime ni les réponses inconnues ni les livraisons répétées qui apparaissent ensuite.

La recette montre comment retrouver une intention après un arrêt de processus, reprendre un bail expiré et refuser l’acquittement d’un ancien worker. Elle démontre aussi deux livraisons pour un seul effet local protégé. Ces preuves restent bornées au mécanisme exécuté et doivent être complétées sur les composants de votre architecture.

Le bon périmètre associe identité stable, transaction vérifiée, consommateur idempotent, journal de reprise et indicateurs de retard. Il permet au support d’expliquer une commande en attente et de la faire avancer sans recréer l’opération. C’est cette continuité qui donne de la valeur au dispositif.

Dawap accompagne le développement et la fiabilisation de vos applications métier pour transformer ces frontières techniques en comportements vérifiables. Le premier chantier consiste à choisir une décision critique, provoquer ses pannes et établir les preuves nécessaires avant d’étendre les traitements asynchrones.

Portrait de Jérémy Chomel

Vos commandes survivent-elles à une panne du relais ?

Partons d’une opération critique pour vérifier ses transactions, ses reprises et les états réellement visibles par vos équipes.

Articles recommandés

Idempotence et reprise sur erreur : éviter les doubles effets Développement web Idempotence et reprise sur erreur : éviter les doubles effets Lire l'article
  • 7 avril 2026
  • Lecture ~15 min

Une réponse perdue ou un message redélivré ne doit pas créer un second paiement, mouvement de stock ou dossier. Cette analyse relie clé métier stable, contrainte unique, résultat mémorisé, outbox, retries et réconciliation afin de rendre les reprises sûres, observables et exécutables sans correction manuelle risquée.

Messenger Symfony : quand la messagerie aide vraiment le run Développement web Messenger Symfony : quand la messagerie aide vraiment le run Lire l'article
  • 6 janvier 2026
  • Lecture ~12 min

Messenger améliore le run lorsque le produit assume le délai entre une commande et son résultat. Messages stables, idempotence, retries qualifiés, états visibles et reprise par le support transforment alors la file en capacité exploitable, sans confondre demande acceptée, effet produit et résultat encore inconnu.

Compte rendu terrain avec heures, pièces et photos en attente de réception et de validation ERP Développement web Rapports terrain hors ligne et ERP Lire l'article
  • 17 septembre 2026
  • Lecture ~15 min

Un rapport terminé sur le téléphone peut rester incomplet au bureau. Reliez heures, pièces et photos à leur réception réelle avant les effets ERP. Huit contrôles fictifs éprouvent pièce manquante, reprise identique, contenu modifié, droit retiré et correction. Le guide aide à choisir un outil terrain existant ou le parcours spécifique qui manque.

Illustration de la rotation des clés de signature et des messages en attente dans Symfony Messenger Développement web Messenger : changer de clé sans bloquer la file Lire l'article
  • 24 octobre 2026
  • Lecture ~20 minutes

La nouvelle release signe ses messages avec une nouvelle clé, mais un ancien worker ne sait pas les vérifier. Un exercice sur le composant Symfony éprouve les étapes de coexistence, le retour arrière et les tâches encore en échec. Il distingue aussi une ancienne clé d’un ancien format de signature, avant tout changement réel.

Rendons vos traitements métier récupérables

Partons d’une opération critique pour vérifier ses transactions, ses reprises et les états réellement visibles par vos équipes.