Systèmes Événementiels Résilients : Le Pattern Transactional Outbox avec RabbitMQ
Éliminer les anomalies de double écriture entre bases de données et brokers de messages avec garantie de livraison.
1. Le piège de la double écriture en microservices
Dès qu’un service doit à la fois persister un état dans PostgreSQL et publier un événement correspondant sur RabbitMQ, vous avez deux systèmes indépendants qui doivent s’accorder sur un résultat qu’aucun des deux ne peut garantir seul. Une implémentation naïve valide la transaction en base, puis appelle le client du broker pour publier. Ce sont deux opérations réseau distinctes, et il n’existe aucune primitive atomique couvrant les deux à la fois, donc quel que soit l’ordre choisi, une fenêtre s’ouvre où l’une réussit et l’autre échoue.
Si le processus plante, si le pod est tué par un OOM-killer, ou si le réseau vers RabbitMQ time-out juste après le COMMIT mais avant la confirmation de publication, vous vous retrouvez avec une ligne en base dont aucun consommateur n’entendra jamais parler. La commande est créée, le paiement est débité, mais le service de fulfillment ne reçoit jamais l’événement OrderCreated parce que la publication n’a tout simplement pas eu lieu. Ce n’est pas un cas marginal : sous charge, les resets de connexion et les basculements de broker sont fréquents, et chacun d’eux est une perte de donnée silencieuse potentielle.
Inverser l’ordre n’aide pas davantage. Publier d’abord, puis committer, et vous risquez l’échec inverse : un consommateur réagit à un événement décrivant une ligne que le COMMIT suivant n’a en réalité jamais persistée, parce que la transaction a été annulée par une contrainte violée ou un deadlock. Un service en aval agit alors sur un fait qui est faux dans votre source de vérité. Aucun des deux ordres n’est sûr, car vous cherchez une atomicité entre une base relationnelle et un broker de messages, et aucune combinaison de retries côté applicatif ne comble cet écart à elle seule.
2. La solution Transactional Outbox
Le pattern transactional outbox fusionne les deux écritures en une seule. Plutôt que de publier vers RabbitMQ dans le chemin de la requête, vous écrivez l’événement comme une ligne dans une table `outbox_events`, dans la même transaction PostgreSQL que celle qui écrit votre état métier. Comme les deux écritures passent par la même transaction ACID, elles réussissent ou échouent ensemble — il n’y a plus de fenêtre où l’une committe sans l’autre. L’atomicité est obtenue en évitant simplement d’avoir besoin d’une atomicité inter-systèmes.
Un processus dispatcher séparé et asynchrone lit ensuite les lignes non publiées de la table outbox et les relaie vers RabbitMQ. Cela découple la fiabilité de votre transaction métier de la disponibilité du broker : si RabbitMQ est indisponible pendant cinq minutes, votre API continue d’accepter des commandes et d’écrire des lignes outbox, et le retard se résorbe simplement une fois la connectivité rétablie. Le broker devient un mécanisme de livraison, pas une dépendance que le chemin de requête doit faire confiance de façon synchrone.

