L'event-driven architecture de Primary
Retour d'expérience après deux ans passés en production

Staff Engineer @Primary. Adepte du Domain Driven Design et de l'event driven architecture, j'utilise ces concepts au quotidien afin de maintenir à long terme la qualité des logiciels sur lesquels je travaille.
En complément des lunch talks que j’ai donnés à Devoxx France, au JUG Summer Camp et à BDX/IO en 2025 sur ce sujet, voici une synthèse concernant l’architecture event-driven du backend de Primary.
Il y a maintenant plus de deux ans, nous sommes partis en production avec une event-driven architecture que nous avons choisi pour de multiples raisons que j’évoquerai dans cet article. Depuis, nous maintenons au quotidien cette architecture et ses outils. Il est temps de faire un premier bilan sur ses avantages et ses inconvénients.
Qu’est ce qu’une Event-Driven Architecture ?
Une architecture de type event-driven est une architecture dans laquelle un module métier va publier le résultat d’un traitement (par exemple la saisie d’une conclusion dans une consultation par un médecin) sous la forme d’un événement métier. On appelle ce module métier un publisher. Cette publication d’événement se fait au travers d’un message broker (RabbitMQ dans notre cas) dans une file d’attente.
Chaque file d’attente peut être écoutée par de multiples consommateurs. On les appelle subscribers ou event handlers. A chaque fois qu’un événement arrive dans la file d’attente, chaque event handler qui s’est abonné à l’événement est notifié et son code est appelé pour consommer l’événement.
Cela garantit un découplage parfait des responsabilités. Les modules ne se connaissent pas à l’avance et n’interagissent qu’au travers de ces événements.

Cela ouvre énormément le champ des possibles en termes d’architecture : on pourrait imaginer faire du monolithe modulaire (spoiler alert : c’est ce qu’on fait chez Primary) ou encore éclater nos modules en microservices si on en avait le besoin.
Quels sont les avantages de cette architecture ?
Notre démarche a été, dès le premier jour, de choisir une architecture event-driven. Ce choix a été fait pour de multiples raisons :
Maintien de la simplicité des modules métier et découplage (le code de notre backend est un monolithe modulaire, autrement appelé modulith)
Orientation métier des événements en accord avec notre démarche de conception orientée métier (Domain-Driven Design). Nous avons fait le choix d’utiliser des événements métier tels que “ConsultationTerminee” ou “BilanDePreventionValide” plutôt que des événements techniques comme par exemple “ConsultationAjoutee” ou “ConsultationModifiee”.
Evolutivité : cela est bien plus pérenne de créer de nouveaux event handlers pour implémenter de nouvelles fonctionnalités que d’ajouter celles-ci dans du code existant.
Scalabilité : c’est très facile pour nous d’ajouter de nouvelles instances de notre backend (scaling en un clic sur Clever Cloud) afin de répondre à des pics de charge.
Disponibilité : le backend reste disponible même si un event handler est défaillant; de plus, il est possible de re-publier des événements après un incident. C’est le sujet dont je parle le plus dans mon lunch talk car je trouve que c’est un des plus gros avantages 🙂
Les challenges rencontrés par l’équipe
La gestion des erreurs
Je trouve que la gestion des erreurs est un des meilleurs arguments de vente de cette architecture : cela nous donne la capacité de récupérer d’une erreur après un incident. Cela rend tout simplement notre système résilient.
Pourtant, cette capacité à réessayer après une erreur a été un de nos plus gros défis.
Dans mon talk, je prends l’exemple d’une notification push que l’on envoie à nos patients sur leur app Primary lorsqu’ils prennent rendez-vous dans un de nos cabinets.

Le problème se pose si l’event handler devient défaillant : que doit-on faire ? On ne doit pas perdre le contexte d’exécution car le patient ne recevrait jamais sa notification push.
Dans ce style d’architecture, on utilise un mécanisme de files d’attente qui permettent d’isoler les événements en erreur afin de pouvoir les retraiter plus tard. Cela s’appelle des Dead Letter Queues.
Concrètement, si l’event handler fait son travail correctement, il informe le message broker de la consommation de l’événement et il peut passer à l’événement suivant dans la file d’attente. Mais s’il y a une erreur, l’événement ayant donné lieu à l’erreur sera alors re-routé vers une Dead Letter Queue afin d’être isolé pour être retraité plus tard.
Chez Primary, lorsqu’un événement part en Dead Letter Queue, on en profite pour ajouter des méta-données dans les entêtes de l’événement :
Stacktrace et message de l’exception
File d’attente d’origine du message (pour nous, un event handler = une file d’attente, un événement = un exchange dans RabbitMQ)
Date à laquelle l’événement a été “refusé” pour la première fois
Nombre de retries
Date d’un rendez-vous rattaché potentiel, nom du cabinet de rattachement, identifiant du profil de patient concerné
ID de la trace de notre APM
Cela nous permet de gagner énormément en observabilité.

Une fois l’incident sur l’event handler terminé, on peut alors ré-injecter l’événement dans le système dans sa file d’attente d’origine. Cette opération pourra être répétée autant de fois que nécessaire, jusqu’à ce que la consommation de l’événement par l’event handler puisse se faire 🙂.

Le traitement des erreurs en production
RabbitMQ, notre message broker, dispose de ce mécanisme de Dead Letter Queue de base, et c’est un bonheur pour garder la maîtrise des erreurs en production.
Néanmoins, la console d’administration de RabbitMQ est assez austère et ne permet pas de gérer ces files d’attente de messages en erreur.
En 2023, dans les tous premiers mois de la construction de notre backend, je prends alors le temps de faire une étude afin de savoir s’il existe des produits sur étagère qui permettent de gérer ces Dead Letter Queues comme on le souhaite. Malheureusement, cela n’existe pas, alors je décide de réaliser une webapp qui permet :
de lister les messages qui sont en Dead Letter Queue
de renvoyer tous les messages vers leurs files d’attente d’origine
Ce projet a été réalisé au départ en moins d’une semaine avec le minimum d’investissement, ce qui veut dire qu’il n’était pas au point lors de la première mise en production, pour de multiples raisons :
Evénements mélangés et en grand nombre
Evénements en doublons
Impossibilité de renvoyer des événements de manière unitaire
Impossibilité de supprimer un événement de la Dead Letter Queue
Pendant quelques mois, nos difficultés principales pour le traitement des erreurs en production ont été ces limitations qui nous ont fait passer beaucoup trop de temps en analyse des problèmes.
Itérer et améliorer notre “Dead Letter UI”
“Dead Letter UI”, c’est le nom que l’on donne en interne à ce projet qui nous permet de superviser la Dead Letter Queue en production (mais aussi sur notre environnement de staging).
Forts d’un atelier Kaïzen qui nous a permis de lister clairement toutes nos difficultés rencontrées, nous avons amorcé un chantier d’amélioration de cette application :
Ajout de la possibilité de visualiser les événements en erreur par event handler
Ajout de la possibilité de renvoyer ou supprimer un événement unitairement ou par event handler
Ajout d’un lien vers la trace de l’exécution du code dans notre APM
Ajout de données de production lors de la mise en Dead Letter Queue qui nous permet de savoir si un événement est lié à un rendez-vous et/ou à un patient ainsi qu’à un cabinet. En bonus, on recrée les liens vers le logiciel médecin et le logiciel qu’utilisent les équipes soignantes

Toutes ces améliorations nous ont permis de sortir la tête de l’eau. Nous avons enfin pu réagir rapidement et de manière éclairée lorsque nous avions des problèmes.
Aujourd’hui, un événement en Dead Letter Queue nous occupe en moyenne une minute. Dans ce temps, nous sommes capables de décider :
Si c’est un événement qui a expiré (et on le supprime)
Si c’est un problème connu (donc pas besoin d’analyse complémentaire)
Si c’est un nouveau problème
En tout cas, nous avons une politique de traitement systématique de ces cas et lorsque notre alerting nous avertit d’une erreur, nous intervenons très rapidement pour retrouver un compteur à zéro.
Le rejeu des événements
Le rejeu systématique des événements est tentant avec un outil tel que celui que nous avons développé; néanmoins, ce n’est pas une option, car les événements que nous produisons peuvent se retrouver obsolètes très vite.
A titre d’exemple, nous envoyons une notification push sur le téléphone du patient lorsqu’il arrive au cabinet. Si une erreur se produit lors de son temps d’attente, nous ne prenons jamais le risque de rejouer ce type d’événement, car cela pourrait produire des side effects au mauvais moment (si le patient est déjà dans le box du médecin par exemple).
Par design, nous avons prévu tous nos event handlers pour qu’ils soient idempotents. De cette façon, nous savons que nous pouvons rejouer un événement si son cadre temporel est correct. L’heure du rendez-vous associé à l’événement présenté par la Dead Letter UI nous permet de prendre la décision en quelques secondes seulement.
Le rejeu des événements n’est donc clairement pas un sujet simple; nous avons choisi pour l’instant de traiter des événements rejetés manuellement; nous pourrions néanmoins automatiser ces traitements, que ce soit :
par une expiration technique au niveau du message broker
par une expiration calculée par nos event handlers
Nous avons pour l’instant fait le choix pragmatique de les traiter manuellement, compte tenu de notre faible volumétrie d’erreurs. Notre stratégie pourra changer bien entendu lorsque la volumétrie de la production évoluera.
Bilan et challenges à venir
Sur les deux dernières années en production, cela fait un peu plus d’une année que notre stack événementielle est suffisante pour nous permettre de gérer la production de manière sereine. Bien entendu, il reste encore des choses à améliorer :
fiabiliser l’envoi des événements dans le message broker. RabbitMQ ne nous a jamais fait défaut en production, mais il serait fâcheux de perdre des événements
augmenter l’observabilité pour pouvoir reconstituer des parcours fonctionnels à partir d’événements (ceux-ci sont isolés pour l’instant)
documenter nos événements pour les futurs développeurs et développeuses qui vont nous rejoindre
absorber la croissance de traffic lorsque nous allons ouvrir de nouveaux cabinets (nous allons doubler le nombre de cabinets en 2026, et ce n’est qu’un début)
Il y a encore énormément de sujets que j’aimerais développer à propos de l’architecture de notre backend. C’est pourquoi il se pourrait bien que vous me revoyiez en 2026 pour parler de ce sujet, plus en détail cette fois 😉.



