Architecture orientée événements
Introduction
Lorsqu’une commande est validée, plusieurs traitements peuvent s'enchaîner : réserver le produits, envoyer une confirmation, préparer une facture, etc. Une première solution consiste à appeler tous les composants concernés dans le traitement de la commande. Cette approche peut devenir problématique lorsque ces opérations s'exécutent avec un délai trop élevé ou évoluent indépendamment.
L’architecture orientée événements propose une alternative permettant aux composants de collaborer entre eux : publier qu'un traitement a eu lieu, puis laisser les composants intéressés réagir en conséquence. Cette approche convient à certaines applications distribuées, mais elle introduit aussi des questions de fiabilité et de cohérence qu’il faut prendre en compte dès la conception.
Présentation
Une architecture orientée événements organise tout ou partie des échanges autour d’événements. Un événement décrit une action passée et significative : OrderValidated, PaymentApproved ou DocumentSent. Le composant qui le produit envoie l'évènement. Puis un ou plusieurs composants le reçoit et exécutent leurs propres traitements.
Dans une mise en œuvre distribuée, un broker de messages ou une plateforme de diffusion transporte les événements entre les applications. Le producteur n’a pas besoin de connaître chaque consommateur. L'exécution du traitement est généralement asynchrone, car la validation de la commande et l’envoi de la confirmation ne se terminent pas nécessairement au même instant. L’approche peut également s’appliquer à l’intérieur d’un monolithe, entre modules : elle n’impose pas d'implémenter des micro-services.
Envoi et réception d'un évènement
Prenons l'exemple d'une boutique en ligne :
- Le module de gestion des commandes valide une commande.
- Il publie un événement nommé
OrderValidated, contenant l’identifiant de la commande. - Le module d'envoi des notifications reçoit cet événement et prépare un mail.
- Le module de gestion des données consolidées reçoit le même événement et met à jour ses indicateurs.
Le module de gestion des commandes ignore quels consommateurs sont abonnés. Chaque consommateur gère son propre traitement. Si une nouvelle fonctionnalité doit réagir à la validation, elle peut être ajoutée sans introduire un appel direct supplémentaire dans le module de gestion des commandes.
Pour simplifier la mise en œuvre de cette architecture, le modèle publish/subscribe permet à plusieurs consommateurs de recevoir un même événement.
Un exemple simple en C#
L’exemple ci-dessous montre la structure des échanges. L’interface représente un mécanisme de publication ; son implémentation concrète prend en charge l'envoi de l'évènement.
public record OrderValidated(
Guid EventId,
Guid OrderId,
DateTimeOffset ValidationDate);
public interface IEvents
{
Task PublishAsync<T>(T eventData, CancellationToken cancellationToken)
where T : notnull;
}
public class OrderService(IEvents events)
{
public async Task ValidateAsync(
Guid orderId,
CancellationToken cancellationToken)
{
// Exemple simplifié : valider et enregistrer la commande ici.
var eventData = new OrderValidated(
Guid.NewGuid(), orderId, DateTimeOffset.UtcNow);
await events.PublishAsync(eventData, cancellationToken);
}
}
Un consommateur peut ensuite réagir à l’événement :
public class ConfirmationHandler(IEmailSender emails)
{
public Task HandleAsync(
OrderValidated eventData,
CancellationToken cancellationToken)
{
return emails.SendConfirmationAsync(
eventData.OrderId, cancellationToken);
}
}
Cet exemple montre un exemple découplage ; il ne suffit pas, à lui seul, pour garantir une publication fiable. Si la commande est enregistrée puis que l’application s’arrête avant la publication, l’événement est perdu. Le gestionnaire présenté peut aussi envoyer deux mails si le message est livré deux fois.
En pratique, les applications .NET utilisent par exemple RabbitMQ ou Azure Service Bus pour certains scénarios de messagerie, et Azure Event Hubs pour l’ingestion de flux volumineux. Des bibliothèques comme MassTransit peuvent aussi être utilisées. Le choix dépend du besoin de diffusion, des capacités et débit et de rétention et de contraintes d'exploitation.
Les avantages
Un couplage faible
Le producteur publie un événement sans connaître les consommateurs qui réagiront.
Des traitements indépendants
Chaque consommateur traite l’événement à son rythme, avec ses propres ressources.
Une meilleure gestion des pics d'activité
La messagerie peut conserver les événements pendant que les consommateurs rattrapent leur retard.
Une extension facilitée
Un nouveau traitement peut s’abonner à un événement sans modifier le producteur.
Les limites
Une cohérence souvent différée
Les données des consommateurs peuvent être temporairement en décalage avec celles du producteur.
Une complexité d’exploitation accrue
Suivre un traitement réparti entre plusieurs composants nécessite des traces, des identifiants de corrélation et des alertes.
Des échecs à gérer explicitement
Les messages peuvent être reçus plusieurs fois, échouer ou arriver dans un ordre inattendu. Des mécanismes de reprise sont nécessaires.
Des contrats à faire évoluer
Modifier le contrat d'un événement peut poser des problèmes aux consommateurs. Ses évolutions doivent être compatibles ou versionnées.
Quand cette architecture est-elle un choix pertinent ?
L’architecture orientée événements est pertinente lorsque plusieurs traitements indépendants doivent réagir à un fait survenu, lorsque certains traitements peuvent être différés. Elle peut aussi faciliter l’intégration de systèmes distincts qui ne doivent pas dépendre directement des systèmes actuels.
Les bonnes pratiques avec .NET
Publier de manière fiable : le pattern Outbox
Enregistrer la modification métier et l’événement dans une même transaction, puis publier l’événement dans un processus séparé.
Rendre les traitements idempotents
Un événement reçu plusieurs fois ne doit être traité qu’une seule fois.
Définir des périmètres métier et des contrats explicites
Publier les évènements aux consommateurs sans exposer des informations sur l'implémentation du producteur.
Prévoir l’ordre, les reprises et l’observabilité
Définissez les besoins d’ordre des événements et prévoyez des traces, des alertes et une procédure de reprise en cas d'échec.
