Architecture orientée événement : réconcilier Notifications et Evénements "Complets"

Paul SANTUS - Jul 4 - - Dev Community

Les architectures orientées événement offrent de nombreux bénéfices : le découplage entre les composants applicatifs permet une meilleure résilience à la panne, un flux de traitement adapté aux ressources disponibles, un faible temps d'attente pour l'utilisateur (pourquoi traiter de façon synchrone ce qu'il peut l'être après lui avoir répondu ?).

Cependant, leur conception n'est pas aisée et peut entraîner de nombreux débats : faut-il des événements très légers, à charge pour le consommateur de demander un éventuel complément d'information ? faut-il des événements complets ? ..

Dans ce billet, j'expose les différents types d'événements et propose une manière de réconcilier simplement les différentes approches en s'appuyant sur AWS EventBridge (un repo Github avec un exemple pleinement fonctionnel vous attend en fin de billet !).

Les différents types d'événements

Un premier pattern est d'utiliser l'événement "Notification", qui contient juste un identifiant. En voici un exemple :

{ "orderId": "1234567" }
Enter fullscreen mode Exit fullscreen mode

Cet événement minimaliste a pour avantage de ne nécessiter aucune connaissance de la modélisation du domaine métier : il y a peu de risque de violer le contrat d'interface notamment en cas d'évolution. Si un consommateur veut en savoir plus, il peut aller chercher la donnée à partir de l'identifiant communiqué (et si le système source présente une API GraphQL, il pourra aller chercher uniquement la donnée qui est nécessaire pour lui).

Une seconde approche se nomme "Event-carried State Transfer" (par analogie avec le "REpresentational State Transfer" qui caractérise les API REST), c'est à dire que l'événement porte l'état courant. En voici un exemple :

{
   "id": "1234567", 
   "status": "PAYMENT_ACCEPTED",
   "customer": "Bob",
   "content": [ ... ]  
}
Enter fullscreen mode Exit fullscreen mode

Ce type d'événement a l'avantage de porter toute l'information disponible et ainsi d'épargner une requête au consommateur, mais aussi d'améliorer les options de filtrage dont on disposera au niveau du bus d'événement : on pourra par exemple choisir de ne consommer que les événements représentant une commande au statut PAYMENT_ACCEPTED (par exemple pour envoyer un mail de confirmation de commande).

Une troisième voie consiste à faire un "Delta", i.e. transmettre également l'état précédent en complément de l'état courant.

Voici un résumé des avantages et limites de chaque approche :

Image description

Réconcilier l'approche Notification et l'approche Event-carried State Transfer

Dès lors, on peut vouloir tirer parti des bénéfices de chaque approche

  • sans pour autant complexifier inutilement l'architecture des applications qui publient les événements comme de celles qui les consomment.
  • sans parfois avoir la main sur le code source des applications.

C'est là qu'une approche serverless mêlant bus d'événement EventBridge et Lambda a tout son sens. Dans cette approche, on va mettre en place

  • sur le bus d'événement des règles qui capturent les événements de type "Notification"
  • et des micro-services d'enrichissement qui vont récupérer la donnée du domaine métier correspondant et republier l'événement enrichi.

Je commencerai par un exemple simple, avant de montrer comment on peut étendre ce pattern. En bas de cet article, vous trouverez un lien vers les deux implémentations qui suivent.

La version simple : un seul enrichissement

Dans cet exemple, une application de gestion de paiement publie un événement de type PAYMENT portant uniquement l'id de l'événement (un événement de notification, donc).

Diagramme d'architecture de la solution

Côté EventBridge, une règle va explicitement viser ces événements en vérifiant qu'aucune donnée supplémentaire n'est associée

{
  "detail-type": ["PAYMENT"],
  "detail.payment_data.id": [{ "exists": false }]
}
Enter fullscreen mode Exit fullscreen mode

Si cette règle capture un événement, elle va l'envoyer vers une Lambda qui va publier le même événement enrichi.

On va donc trouver successivement dans le bus d'événement deux événements (ayant le même id métier) :

  • la notification
{
    "version": "0",
    "id": "a23a7513-b67a-d455-f90c-1f9ddbd14820",
    "detail-type": "PAYMENT",
    "source": "PaymentSystem",
    "account": "112233445566",
    "time": "2024-07-04T09:06:47Z",
    "region": "eu-west-1",
    "resources": [],
    "detail": {
        "id": "2237082"
    }
}
Enter fullscreen mode Exit fullscreen mode
  • et l'événement complet (ECST):
{
    "version": "0",
    "id": "51bbf35e-97d8-8f80-1cc2-debac66460e6",
    "detail-type": "PAYMENT",
    "source": "PaymentSystem",
    "account": "112233445566",
    "time": "2024-07-04T09:06:49Z",
    "region": "eu-west-1",
    "resources": [],
    "detail": {
        "id": "2237082",
        "payment_data": {
            "id": "2237082",
            "type": "Credit",
            "description": "Credit Card - HSBC",
            "status": "Confirmed",
            "state": "Paid",
            "value": 1700,
            "currency": "EUR",
            "date": "2018-12-15"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

(la structure d'événement est un peu plus complexe que dans la partie théorique : on retrouve ici la structure typique d'un événement EventBridge qui encapsule le contenu métier.)

Cet événement "pleinement qualifié" est ensuite envoyé

Le bus d'événement EventBridge fournit notamment :

  • Le découplage entre Producteurs et Consommateurs avec un middleware scalable et hautement disponible
  • Une logique avancée de filtrage/capture d'événements
  • La possibilité de logger les événements et de les archiver pour re-jeu
  • La gestion du retry pour les invocations synchrones faites par le bus d'événement.
  • La transformation de l'événement pour l'adapter au format attendu par le consommateur, sans avoir à déployer cette transformation dans une fonction Lambda.

L'ensemble de ces fonctionnalités sont démontrées dans le code disponible en fin d'article.

Un enrichissement plus complexe

Imaginons un cas plus complexe : le système de paiement publie un événement de paiement. Mais ce paiement est lié à une commande, qui a son propre cycle de vie, géré dans une plusieurs autres applications. Et cette commande est liée à un client, qui a également un cycle de vie propre, géré dans un CRM.

Ici on va mettre en place une logique de filtrage / capture plus complexe, mais le code des fonctions d'enrichissement ne change pas !

Image description

Démo / mise en oeuvre concrète !

Nous avons démontré dans ce blog post comment réconcilier les deux principaux modèles de gestion d'événements, grâce à AWS EventBridge et AWS Lambda

Vous trouverez dans ce repo Github deux modèles CloudFormation permettant de déployer ces exemples pleinement fonctionnels.


Vous souhaitez démarrer dans l'architecture orientée événements et vous avez besoin d'appui ? TerraCloud est là pour vous aider ! Contactez-moi !

Image description

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .