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 :
- Découper un processus et déclencher des tâches asynchrones.
- 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 :
-
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. -
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 :
- 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".
- 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.
- 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.
{
"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 :
- La brique de paiement publie l'événement "paiement terminé pour la commande n°42" sur un sujet (topic) dédié.
- La brique anti-fraude s'est abonnée à ce topic. Elle reçoit l'événement et lance son analyse.
- La brique financière, elle aussi abonnée au même topic, reçoit le même événement et met à jour ses comptes.
- 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.