Chapitre 7.4Communication asynchrone

Push, files de messages et streaming d'événements.

8 minutes de lecture

Lors de l'introduction aux API, nous avons vu qu'un système informatique est constitué de dizaines de briques logicielles et de composants qui doivent communiquer entre eux. Leur API représente l'ensemble des contrats, des fonctionnalités, qu'ils exposent publiquement.

Jusqu'à présent, avec REST et GraphQL, nous avons parlé des entrées d'un composant, c'est-à-dire ce qu'il expose pour être directement appelé.

Exemple : "envoie-moi la requête HTTP GET /offre/42 et je te retournerai les détails de l'offre n°42".

Nous allons voir dans ce chapitre qu'un composant peut aussi produire des sorties. C'est lui qui déclenche alors la communication, pour émettre une information sans que personne ne la lui ait demandée. Les développeurs s'en servent pour :

  1. Découper un processus et déclencher des tâches asynchrones.
  2. Séparer les responsabilités entre les composants, pour construire des systèmes indépendants et maintenables sur le long terme.

Souviens-toi de cette phrase "Leur API représente l'ensemble des contrats, des fonctionnalités, qu'ils exposent publiquement.". Tous les messages, en particulier les événements métiers, entrent dans cette définition.

Comme pour toute communication sortante :

  • Il faut mettre en place une stratégie de modification pour assurer la rétrocompatibilité : versioning, interdiction de supprimer un champ...
  • Elle doit être documentée.
  • Elle peut être monétisable.
  • Elle doit être pensée comme un produit intuitif et répondant à un besoin.

Le point de départ, la notification directe

Partons directement sur un exemple :

Nous avons un site d'e-commerce, le client vient de payer, il faut maintenant lancer toute la chaîne de traitement de sa commande : analyse anti-fraude, envoi du colis...

  • Chaque maillon de cette chaîne est développé et maintenu par une équipe différente de développeurs, dans une brique logicielle dédiée.
  • La chaîne est séquentielle : les traitements s'enchaînent dans l'ordre, l'un des maillons peut stopper la progression.
  • Tout ce traitement doit être asynchrone de la navigation du client. Maintenant qu'il a payé, il peut sortir du parcours, nous le recontacterons si besoin.

La brique de paiement a donc terminé son traitement et veut passer la main à la brique anti-fraude.

On peut penser cette architecture de différentes façons. Il y a 15 ans, on serait sûrement passé par une source de données partagée ou un export dans un fichier. Un truc du style : dans la base de données, le paiement passe le statut de la commande en "paiement validé", l'anti-fraude vient ensuite requêter toutes les deux minutes les commandes avec ce statut.

Ça fonctionne, mais c'est loin d'être optimal :

  • C'est l'équivalent d'un enfant qui demande en boucle si on est bien arrivé.
  • Ça ralentit la chaîne de potentiellement deux minutes. S'il y a vingt étapes, ça décale d'autant la livraison du colis.
  • La base de données est une adhérence commune entre les deux briques. Et ça, c'est mal ! Nous reviendrons sur ce point dans le chapitre sur les microservices.

Il serait donc plus intéressant de mettre en place un système dans lequel le paiement pousse directement l'information à l'anti-fraude.

Ces notifications push sont possibles avec une simple requête HTTP. Le paiement appelle directement l'API de l'anti-fraude : "Hey, anti-fraude, vérifie que le client de la commande n°42 n'est pas un fraudeur".
Simple et efficace, c'est ce qu'on fait pour l'immense majorité des communications entre briques !

Néanmoins, cette approche a deux inconvénients majeurs :

  1. Alors qu'il est plus haut dans la chaîne, le paiement a une adhérence forte et manipule directement l'API de l'anti-fraude. Lui, il a terminé sa part du travail, ce n'est normalement pas sa responsabilité de savoir qui va prendre le relais et comment.

    Pour inverser le sens de cette dépendance, on peut mettre en place un système d'observation : le paiement donne de la visibilité aux autres briques sur ses événements internes. Il peut proposer, via son API, la possibilité de s'inscrire à "j'ai terminé de traiter cette commande". À chaque événement, il enverra la même requête HTTP à tous les observateurs, sans savoir qui ils sont. C'est à eux de s'adapter pour comprendre et traiter la requête.
  2. Les requêtes HTTP sont synchrones, le paiement se met en pause tant que l'anti-fraude ne lui a pas répondu.

    En l'occurrence, dans notre cas, puisque le paiement se fiche de connaître la suite, il n'attend aucune réponse de la part de l'anti-fraude. L'idéal pour lui serait que l'anti-fraude stocke cette notification et accuse directement réception le plus rapidement possible sans lancer de traitement. Ça obligerait l'anti-fraude à gérer en interne deux étapes : stockage, puis détection des nouvelles entrées pour lancer le traitement. Oups, on retrouve les mêmes problématiques qu'il y a 15 ans avec des requêtes toutes les deux minutes.

On utilise particulièrement ce système d'observation et la séparation en deux étapes lorsqu'on travaille avec des partenaires et qu'HTTP est l'une des seules options. Tu as forcément déjà croisé son nom, on appelle ça un webhook. Tu enregistres chez le partenaire une URL de ton système, et il vient l'appeler à chaque fois qu'un événement t'intéresse.
Exemple : Apple appelle mon serveur pour me prévenir que le client a annulé son abonnement iOS. Stripe fait pareil pour me signaler un paiement contesté, et GitHub pour me prévenir qu'un développeur a poussé du code.

De meilleurs choix s'offrent à nous pour faire communiquer de façon asynchrone des briques internes indépendantes.

Les files de messages

Imagine le tapis roulant quand tu déposes tes bagages à l'aéroport. Une personne pose des valises sur le tapis, une autre est à l'affût pour les mettre dans un véhicule dès qu'elles arrivent. Les deux font leur travail en toute indépendance, sans réellement savoir ce qu'il se passe à l'autre bout. Le gestionnaire de l'aéroport gère plein de tapis.

C'est le principe de la file de messages :

  1. La brique de paiement transmet un message via un courtier de messages, un logiciel qui gère des files. Elle lui demande de produire sur la file "paiement terminé" le message "commande n°42".
  2. Ce courtier, les plus connus étant RabbitMQ et les services managés des hébergeurs comme Amazon SQS, s'occupe de stocker les messages jusqu'à ce que quelqu'un les lise ou jusqu'à leur expiration.
  3. La brique anti-fraude se branche en continu sur la file "paiement terminé". Elle reçoit les messages non lus au fur et à mesure et peut les consommer à son rythme.

Le message lui-même n'a rien de mystérieux, c'est le plus souvent un simple bout de JSON.

Un message déposé sur la file
{
    "evenement": "paiement_termine",
    "commande": 42,
    "montant": 89.99,
    "devise": "EUR",
    "date": "2026-03-01T09:32:00+01:00"
}
Ce logiciel est une dépendance commune entre les deux briques, en quoi c'est mieux que la base de données qu'on utilisait il y a 15 ans ?

Les deux briques sont en effet dépendantes du courtier et de la file, mais c'est une adhérence purement technique.
Le système utilisant le statut des commandes dans la base de données crée, lui, une adhérence basée sur un concept métier, et c'est beaucoup plus fragile et susceptible d'évoluer.

Passer par un courtier offre aussi plusieurs avantages techniques : rapidité, gestion d'une charge importante, garantie de livraison...

La file comme amortisseur

Cette histoire de charge mérite qu'on s'y arrête, parce que c'est souvent la raison numéro un d'installer une file de messages, bien avant les considérations d'architecture.

Imagine ton opération marketing du vendredi matin. Tu envoies la newsletter, et en trois minutes tu prends dix fois ton trafic habituel. Sans file, chaque paiement déclenche immédiatement une analyse anti-fraude, et l'anti-fraude s'écroule sous le nombre. Avec une file, les messages s'empilent tranquillement et l'anti-fraude les traite au rythme qu'elle peut tenir. Les clients, eux, n'ont rien vu, leur paiement est passé instantanément.

La file joue le rôle d'un amortisseur entre une production irrégulière et une consommation régulière. C'est aussi ce qui te donne un indicateur en or pour la supervision, la taille de la file. Si elle grandit sans jamais redescendre, c'est que ton consommateur est durablement trop lent, et tu le sais avant que le client ne s'en plaigne.

Ce qu'il faut savoir avant de se lancer

Un système de files déplace la complexité plutôt que de la supprimer. Trois surprises attendent systématiquement les équipes qui découvrent le sujet.

  • Un message peut arriver deux fois : le consommateur confirme au courtier qu'il a bien terminé son travail, ce qu'on appelle l'acquittement. Si sa machine tombe entre le traitement et l'acquittement, le courtier considère le message comme non traité et le redistribue. La quasi-totalité des courtiers garantissent donc "au moins une fois", pas "exactement une fois". Chaque traitement doit être écrit pour supporter d'être rejoué, sinon tu rembourses deux fois le même client.
  • L'ordre n'est pas garanti : dès qu'on met plusieurs consommateurs en parallèle pour aller plus vite, rien ne dit que "commande créée" sera traitée avant "commande annulée". Les courtiers proposent des mécanismes pour garantir l'ordre à l'intérieur d'un même groupe de messages, mais c'est un choix qui coûte en performance.
  • Les messages empoisonnés : un message qu'un consommateur n'arrive jamais à traiter revient en boucle et bloque tout le reste. On le met donc de côté après quelques tentatives, dans une file spéciale surnommée la file des lettres mortes, où une équipe viendra l'examiner à tête reposée.

Le corollaire de tout ça, c'est que le débogage devient nettement plus difficile. Avec un appel HTTP direct, tu suis le fil d'un bout à l'autre. Avec des files, chaque brique fait son travail dans son coin sans vue d'ensemble. Il faut donc glisser un identifiant unique dans chaque message et le propager de bout en bout, pour pouvoir reconstituer le parcours complet d'une commande dans les journaux.

Un seul consommateur par message

Ce système a une caractéristique déterminante. Un message déposé sur une file n'est délivré qu'à un seul consommateur, et il disparaît de la file une fois traité.

Attention à ne pas se tromper sur ce que ça veut dire. Rien n'empêche de brancher dix exemplaires de l'anti-fraude sur la même file pour traiter dix fois plus de messages en parallèle, c'est même très courant. Mais chaque message ne sera traité que par l'un d'entre eux. La file distribue le travail, elle ne le duplique pas.

Complétons notre exemple :

En plus de l'anti-fraude, on veut tenir au courant une brique financière qu'un paiement est terminé.

Si on continuait à utiliser un simple système de files, il nous faudrait déposer le message dans deux files. Ça prendrait plus de place et, surtout, ça enlèverait l'indépendance de la brique de paiement, en l'obligeant à adapter son comportement en fonction des consommateurs de ses messages. Et il faudrait recommencer à chaque nouveau destinataire.
Pas top !

Les courtiers de messages ont bien une réponse à ça. RabbitMQ, par exemple, n'expose pas directement ses files aux producteurs, il place devant elles un aiguilleur, appelé "exchange", qui peut recopier chaque message dans autant de files qu'il y a d'abonnés. Le producteur ne connaît que l'aiguilleur, et ce sont les consommateurs qui viennent y accrocher leur propre file. L'indépendance est sauvée.

Retiens donc la distinction, plus utile que les noms de produits. Les files de messages brillent quand on découpe un traitement en étapes asynchrones, avec un destinataire par étape. Ce que nous cherchons à faire ici s'apparente plutôt à l'émission d'un événement "paiement terminé", que plusieurs briques pourront écouter, y compris des briques qui n'existent pas encore. Pour ça, il existe un système un peu plus évolué.

Le streaming d'événements

Imagine maintenant un tableau d'affichage dans une gare. Les trains sont annoncés en continu, et chaque voyageur regarde les informations qui le concernent. L'afficheur ne sait pas qui regarde, et les voyageurs ne se connaissent pas entre eux. L'information reste affichée pendant un certain temps, permettant à chacun de la consulter à son rythme.

C'est le principe du streaming d'événements. Le logiciel le plus connu dans ce domaine est Apache Kafka, développé chez LinkedIn puis rendu open source en 2011. On le retrouve aujourd'hui dans une grande partie des architectures qui manipulent de gros volumes d'événements.

Contrairement à une file de messages classique, Kafka conserve les messages après leur lecture, pendant une durée que tu configures. Plusieurs consommateurs peuvent donc lire les mêmes événements, chacun à son propre rythme, sans que les messages soient détruits. Chacun garde simplement un marque-page qui indique où il en est de sa lecture.

Cette conservation a une conséquence spectaculaire. Comme l'historique reste disponible, une brique peut repartir du début et rejouer tous les événements passés. Si l'anti-fraude sort une nouvelle version de son algorithme, elle peut réanalyser six mois de paiements en relisant simplement le flux. Une file de messages classique, elle, aurait tout détruit au fur et à mesure.

Reprenons notre exemple e-commerce. Avec Kafka :

  1. La brique de paiement publie l'événement "paiement terminé pour la commande n°42" sur un sujet (topic) dédié.
  2. La brique anti-fraude s'est abonnée à ce topic. Elle reçoit l'événement et lance son analyse.
  3. La brique financière, elle aussi abonnée au même topic, reçoit le même événement et met à jour ses comptes.
  4. Demain, si une nouvelle brique "fidélité" veut aussi réagir aux paiements, elle n'a qu'à s'abonner. La brique de paiement n'a rien à changer.

C'est le modèle publish / subscribe (pub/sub) : un producteur publie des événements sans se soucier de qui les consomme. Les consommateurs s'abonnent aux sujets qui les intéressent. Tout le monde est indépendant.

Ce modèle est devenu la colonne vertébrale de beaucoup d'architectures modernes. Il permet de construire des systèmes réactifs où chaque brique fait son travail sans connaître les autres. C'est un vrai changement de perspective, on passe d'un système où les briques s'appellent directement à un système où elles réagissent aux événements du monde qui les entoure.

Que choisir, concrètement

Résumons, parce que les trois options se ressemblent de loin et n'ont pourtant pas du tout le même usage.

  • L'appel HTTP direct : l'émetteur connaît son destinataire et attend sa réponse. Simple, immédiat, facile à déboguer. C'est le bon choix par défaut, et de loin le plus fréquent.
  • La file de messages : un travail à faire, un destinataire, mais plus tard et sans bloquer l'émetteur. Idéale pour encaisser des pics de charge et découper un traitement en étapes.
  • Le streaming d'événements : un fait qui vient de se produire, annoncé à la cantonade, que n'importe quelle brique présente ou future peut écouter et rejouer.

Un conseil pour finir. L'asynchrone est séduisant sur le papier, il donne l'impression de tout découpler et de tout rendre extensible. Mais il a un coût très réel en supervision, en débogage et en compétences dans l'équipe. De mon expérience, la plupart des projets qui dégainent Kafka dès le premier jour auraient vécu beaucoup plus heureux avec trois appels HTTP bien placés. Attends d'avoir un vrai problème de charge ou un vrai besoin de plusieurs destinataires, il se manifestera tout seul et il ne sera pas trop tard.

Ces mécanismes prennent tout leur sens dans les architectures découpées en petits services indépendants, le sujet du chapitre sur les microservices.

PrécédentLa programmation objet Tous les chapitres