BUILDING TO SCALE
FR | EN
Parler de votre projet

Événements et commandes : démêler l'architecture orientée événements dans un système Rails

Deux contrats très différents se cachent derrière l'étiquette « orienté événements ». Bien choisir les noms détermine le comportement du reste de l'architecture.

Publié le 13 octobre 2024 · 5 min de lecture

« Orienté événements » sert d'étiquette à deux architectures qui se comportent très différemment sous charge et en cas de panne. Avant de nommer une queue ou un topic, il vaut la peine de préciser laquelle des deux vous construisez réellement.

Une commande ordonne, un événement informe

Une commande nomme une action que vous voulez voir exécutée et s'attend à ce qu'elle le soit : CreateInvoice, CancelSubscription. Elle est adressée à un seul destinataire, qui a le droit de la refuser. Un événement décrit quelque chose qui s'est déjà produit, au passé : OrderPlaced, InvoicePaid. Il n'est adressé à personne en particulier ; l'émetteur ne sait pas, et ne devrait pas se soucier de savoir, qui écoute.

COMMANDE (IMPÉRATIF, UNE SEULE CIBLE) SERVICE A CreateInvoice SERVICE B ÉVÉNEMENT (PASSÉ, AUTANT D'ABONNÉS QUE VOULU) SERVICE A OrderPlaced SERVICE B SERVICE C SERVICE D
Même infrastructure, deux contrats très différents

Là où les équipes se brûlent

L'erreur la plus courante consiste à publier quelque chose qui porte un nom d'événement mais qui a la forme d'une commande, « SendWelcomeEmail » par exemple, puis à s'étonner que le service d'une autre équipe agisse dessus d'une manière qui couple les calendriers de livraison des deux équipes. Si le comportement d'un consumer est nécessaire pour que votre processus métier soit correct, vous émettez une commande et devez traiter la garantie de livraison en conséquence. Si le processus est correct que ce consumer réagisse ou non, c'est véritablement un événement.

Rails, ActiveSupport::Notifications et leurs limites

À l'intérieur d'un seul process Rails, ActiveSupport::Notifications vous offre le pattern événementiel sans effort : on publie une fois, zéro ou plusieurs abonnés réagissent, et aucun d'eux ne bloque les autres ni l'émetteur.

ActiveSupport::Notifications.instrument("order.placed", order_id: order.id)

ActiveSupport::Notifications.subscribe("order.placed") do |*, payload|
  LoyaltyPoints.credit(payload[:order_id])
end

Ce pattern fonctionne très bien dans un même process et s'effondre dès qu'un abonné doit survivre à un redémarrage ou vivre dans un autre service. C'est le moment de recourir à un transport durable, et celui où le pattern outbox prend tout son sens.

Le nommage est la décision de conception

Dans la pratique, la revue d'architecture qui compte le plus n'est pas « Kafka ou RabbitMQ ». C'est d'obliger chaque équipe à écrire, pour chaque message, si elle émet une commande ou annonce un fait. Tout le reste, retries, ordre, rejeu, découle de cette réponse.