backend
L'outbox transactionnel
L'outbox transactionnel
Voici la forme du bug que j'essayais de concevoir pour l'éliminer.
Quelqu'un soumet une demande. Mon handler écrit une ligne en base, puis appelle un service d'e-mail pour envoyer la confirmation. Deux étapes. Elles ont l'air d'une seule opération dans le code, mais elles ne sont pas atomiques — rien ne les lie. Si l'API d'e-mail a cinq mauvaises secondes juste après mon commit en base, cet e-mail de confirmation est simplement parti. Aucune erreur ne remonte à l'utilisateur. Aucun enregistrement n'existe qu'il était dû. La base dit joyeusement « soumis » et la personne attend un e-mail qui n'arrivera jamais.
J'ai rencontré ça assez souvent pour arrêter de le résoudre fonctionnalité par fonctionnalité et adopter le pattern outbox partout.
Le correctif
Écris l'intention en base dans la même transaction que le changement métier :
[une transaction en base]
1. INSERT INTO applications (...)
2. INSERT INTO outbox_events (event_type: 'application.submitted', payload_json: {...})
[commit — les deux atterrissent, ou aucun]
[un processus de fond séparé, qui poll WHERE status = 'pending']
→ tente la livraison
→ au succès : status = 'sent'
→ à l'échec : attempts += 1, réessai avec backoff
La ligne d'événement porte le type d'agrégat, l'id d'agrégat, le type d'événement, la charge et le statut. Parce qu'elle est écrite dans la même transaction que la chose qui l'a déclenchée, les deux réussissent ou échouent vraiment ensemble — je m'appuie sur les garanties propres de la base au lieu d'essayer de coordonner deux systèmes à la main.
Une livraison échouée est maintenant une ligne posée en pending ou failed. Visible. Requêtable. Réessayable. Quelque chose sur quoi je peux alerter. Pas un trou silencieux.
Ce que ça coûte
- La latence. L'effet de bord se déclenche au cycle de poll suivant, pas dans la requête. Mauvais outil pour tout ce qui doit vraiment être synchrone.
- Une pièce mobile de plus. Le dispatcher lui-même doit tourner et être surveillé. S'il meurt discrètement, les événements s'empilent en
pendingpendant que chaque écriture en base a toujours l'air parfaitement saine — j'ai appris à surveiller la profondeur de la file exactement pour ça. - Au moins une fois, pas exactement une fois. Un crash entre « livré » et « marqué envoyé » peut doubler une livraison, donc les effets de bord ont intérêt à être idempotents plutôt que de supposer qu'ils s'exécutent une fois.
Ça vaut la complexité quand perdre l'effet de bord compte vraiment — une confirmation que quelqu'un attend, une notification liée à une vraie étape de workflow, un webhook dont un autre système dépend. Ça ne la vaut pas pour des choses au mieux-effort que personne ne remarque si elles sautent de temps en temps.