Be Software Architecture Wise
Be Wise Apps

Read in English · Leer en español · Auf Deutsch lesen

Section 6.5 sur 6.8 · Intermédiaire · 30 min de lecture

Communication synchrone et asynchrone : REST, gRPC, GraphQL et messagerie

Leçon vidéo

5 min · narré en anglais, avec sous-titres
Écouter seulement (audio)
Lire la transcription

Une fois qu’un système est découpé, ses parties doivent dialoguer à travers un réseau, et la façon dont elles dialoguent façonne le système autant que la façon dont il est découpé. Dans cette leçon, vous allez découvrir les deux modes de base, les principales technologies, et comment concevoir des API qui durent.

Les services dialoguent de deux façons. Un appel téléphonique est synchrone : vous restez en ligne jusqu’à obtenir une réponse, et s’il n’y a personne, vous n’obtenez rien. Une lettre est asynchrone : vous la postez et vaquez à vos occupations, et la réponse arrive plus tard. Les appels téléphoniques sont simples, mais lient les deux personnes au même moment. Les lettres sont plus souples, mais vous ne savez pas tout de suite si votre demande a abouti. Dans les deux cas, les deux parties ont besoin d’un accord écrit et partagé sur ce qui peut être demandé, et d’un moyen de le modifier sans gêner ceux qui en dépendent.

Sur la ligne du haut, l’app client appelle le service des commandes, qui appelle le service de paiement, et chaque étape attend. Pendant qu’il attend, le service des commandes est bloqué. Les trois doivent être disponibles au même moment : les latences s’additionnent et les disponibilités se multiplient. C’est le couplage temporel. Sur la ligne du bas, le service des commandes envoie un message « commande passée » à une file, et poursuit son travail. Le message attend que le service de notifications soit prêt : le destinataire peut donc être occupé ou brièvement indisponible sans que l’émetteur s’en aperçoive. Mais l’asynchronisme déplace la complexité au lieu de la supprimer. Les deux parties dépendent toujours du format et de la signification du message. Une bonne règle : n’appelez de façon synchrone que lorsque l’appelant ne peut vraiment pas continuer sans la réponse.

REST, pour Representational State Transfer, utilise les ressources, les méthodes et les codes d’état tels que HTTP les définit. Il se met bien en cache avec des outils standard, et convient aux API publiques et partenaires. gRPC envoie des messages binaires compacts, avec du code typé généré à partir d’un fichier de contrat. Il est apprécié pour les appels internes entre services, mais les navigateurs ne peuvent pas l’appeler directement, et ses messages sont difficiles à lire dans les journaux. GraphQL permet à chaque client de demander exactement les champs dont il a besoin, en un seul aller-retour. Mais l’équipe serveur doit limiter les requêtes coûteuses et vérifier les autorisations champ par champ. Enfin, la messagerie, au moyen de files et de topics, convient au travail qui peut être fait plus tard, au fan-out vers de nombreux consommateurs et au lissage de la charge.

Passons aux nouvelles tentatives. En HTTP, GET ne fait que lire : on peut donc le répéter sans risque. PUT et DELETE sont idempotentes. Mais POST n’est ni l’un ni l’autre : le rejouer peut donc créer une seconde commande et débiter le client deux fois. La solution est une clé d’idempotence. L’app envoie sa commande avec une clé unique. L’API enregistre ensemble la commande et la clé, et la base de données confirme la commande mille quarante-deux. Mais la réponse se perd au retour : l’app atteint son délai d’expiration et fait une nouvelle tentative. La nouvelle tentative porte la même clé. L’API constate que la requête a déjà été traitée, et renvoie la réponse stockée pour la commande mille quarante-deux. Pas de seconde commande, pas de second débit.

L’exemple commenté de la section ajoute trois règles. Si le client appuie de nouveau pendant que la première requête est encore en cours, le service répond par un conflit, et l’app attend avant de réessayer. Deux copies ne s’exécutent jamais en même temps. Si un bogue envoie la même clé avec un panier différent, le service la rejette, au lieu de renvoyer silencieusement la mauvaise commande. Enfin, les clés expirent après une durée publiée, ici vingt-quatre heures. La clé est créée une seule fois par opération logique, et non à chaque tentative, et enregistrée dans la même transaction que la commande, si bien qu’un plantage ne peut pas les séparer.

Une API qui a des consommateurs est une promesse. Les changements rétrocompatibles, comme un champ facultatif ou un nouvel endpoint, peuvent être faits librement. Supprimer ou renommer un champ casse les consommateurs. Changer la signification d’un champ est la rupture la plus dangereuse, car rien n’échoue visiblement. Les consommateurs y contribuent en étant des lecteurs tolérants : ils ignorent les champs qu’ils ne comprennent pas, si bien que les ajouts ne les cassent jamais. Quand une rupture est inévitable, versionnez l’API. Entre vos propres services, vous pouvez plutôt procéder par expansion-contraction : ajouter le nouveau champ, faire migrer chaque consommateur, puis supprimer l’ancien. Et les tests de contrat pilotés par le consommateur permettent à chaque consommateur de déclarer ce qu’il utilise réellement, si bien que le build du fournisseur montre quels changements casseraient qui. C’est ce qui rend le déploiement indépendant sûr.

La section complète traite de REST tel qu’il a été défini à l’origine, de la pagination, des formats d’erreur et des outils de contrat. Ensuite, dans la section six point six, nous verrons les modèles de cohérence, CAP et PACELC. À très vite.

Ce qu’il faut savoir

Deux façons de dialoguer, et le couplage que chacune crée

Une fois qu’un système est découpé en parties déployées séparément (voir pourquoi distribuer), ces parties doivent dialoguer à travers un réseau, et la façon dont elles dialoguent façonne le système autant que la façon dont elles sont découpées. Il existe deux modes de base. Dans la communication synchrone, l’appelant envoie une requête et attend la réponse avant de continuer, comme lors d’un appel téléphonique. Dans la communication asynchrone, l’émetteur confie un message à un intermédiaire, généralement un courtier de messages (message broker), et poursuit son travail ; le destinataire récupère le message quand il est prêt, comme une lettre dans une boîte aux lettres. La section 0.6 a présenté la différence ; elle est examinée ici en tant que décision architecturale.

Les appels synchrones créent un couplage temporel : les deux parties doivent être en fonctionnement et joignables au même moment. La disponibilité de l’appelant est donc au mieux égale au produit des disponibilités de ses dépendances, et son temps de réponse, y compris sa latence de queue (tail latency), contient le leur : c’est l’arithmétique de la section 6.1. Un service de paiement lent rend le service des commandes lent, et si tous les threads du service des commandes l’attendent, le service des commandes cesse de répondre à qui que ce soit. Les délais d’expiration, les nouvelles tentatives et les disjoncteurs limitent les dégâts ; ils sont traités dans les patrons de résilience.

La messagerie asynchrone supprime le couplage temporel : le courtier stocke le message, si bien que le destinataire peut être lent, occupé ou brièvement indisponible sans que l’émetteur s’en aperçoive. Elle ne supprime pas toutes les formes de couplage. Les deux parties dépendent toujours du format et de la signification du message (couplage de format et couplage sémantique), ainsi que du courtier lui-même, qui devient une infrastructure critique. L’émetteur perd aussi la connaissance immédiate du résultat : la conception doit donc gérer le travail qui échoue plus tard, les messages qui arrivent deux fois et ceux qui arrivent dans un ordre différent. L’asynchronisme déplace la complexité au lieu de la supprimer.

Une règle empirique utile : utilisez un appel synchrone quand l’appelant ne peut vraiment pas continuer sans la réponse (« cette carte est-elle autorisée ? »), et la messagerie asynchrone quand l’appelant a seulement besoin de savoir que le travail sera fait (« envoyer le reçu », « prévenir le restaurant »). Beaucoup de bonnes conceptions gardent le chemin côté utilisateur court et synchrone, et placent tout le reste derrière une file.

Le couplage temporel, avec et sans courtier

Une chaîne synchrone où chaque service attend le suivant, comparée à une transmission asynchrone via une fileSynchrone : chaque étape attendApp clientrequêteServ. commandesbloqué, en attenterequêteService paiementLes trois doivent tourner en même temps ; latences additionnées, dispos multipliéesAsynchrone : un courtier garde le messageServ. commandesenvoiFileOrderPlaced attendquand prêtNotifications(service)L’émetteur continue ; le récepteur peut être arrêté un moment sans que l’émetteur le voie
Dans une chaîne synchrone, l’appelant est bloqué et dépend de chaque service en aval ; avec un courtier entre les deux, l’émetteur et le destinataire n’ont plus besoin d’être disponibles au même moment.

REST tel que Fielding l’a défini

REST (Representational State Transfer) a été défini par Roy Fielding dans sa thèse de doctorat de 2000 comme un style architectural pour les grands systèmes hypermédias en réseau : le web lui-même. Fielding l’a construit comme un ensemble de contraintes, dont chacune apporte une qualité. Le client-serveur sépare l’interface utilisateur du stockage des données, afin que les deux évoluent indépendamment. Une interaction sans état signifie que chaque requête contient tout ce qu’il faut pour la comprendre, si bien que n’importe quelle instance de serveur peut la traiter, ce qui favorise la scalabilité et la reprise. Des réponses pouvant être mises en cache indiquent si elles peuvent être réutilisées, ce qui réduit la latence et la charge. Une interface uniforme (des ressources identifiées par des URI, manipulées au moyen de représentations, des messages autodescriptifs, et l’hypermédia comme moteur de l’état de l’application) permet à des intermédiaires génériques, comme les caches et les proxys, de comprendre le trafic. Un système en couches permet à des proxys, des passerelles et des équilibreurs de charge de s’insérer sur le chemin de façon invisible. Le code à la demande, comme le JavaScript envoyé à un navigateur, est la seule contrainte facultative.

La plupart des API dites RESTful ne satisfont que certaines de ces contraintes. Le modèle de maturité de Leonard Richardson, présenté en 2008 et popularisé par Martin Fowler en 2010, en décrit les étapes. Au niveau 0, HTTP n’est qu’un tunnel : chaque appel est un POST vers un seul endpoint, avec l’opération nommée dans le corps, comme en SOAP. Au niveau 1, l’API a des ressources distinctes (/orders/1042, /restaurants/7). Au niveau 2, elle utilise les méthodes et les codes d’état HTTP tels que HTTP les définit. Au niveau 3, les réponses portent des contrôles hypermédias : des liens qui indiquent au client ce qu’il peut faire ensuite (une commande passée contient un lien pour l’annuler, jusqu’à ce que l’annulation ne soit plus permise).

Les praticiens sont réellement en désaccord sur le niveau 3. Fielding a toujours soutenu qu’une API sans hypermédia n’est pas du REST. La plupart des API publiques s’arrêtent au niveau 2, car leurs clients sont écrits d’après la documentation plutôt que découverts à l’exécution, et le bénéfice du suivi des liens compense rarement la conception supplémentaire. Une lecture pragmatique est que le niveau 2, appliqué de façon cohérente, apporte l’essentiel de la valeur ; l’hypermédia se justifie quand le serveur doit guider de nombreux clients écrits indépendamment à travers un processus qui évolue.

La sémantique HTTP qui compte pour l’architecture

La sémantique de HTTP est définie dans la RFC 9110 (2022). Deux propriétés des méthodes comptent plus que toutes les autres pour la conception distribuée. Une méthode sûre ne fait que lire : GET, HEAD et OPTIONS ne modifient rien que le client ait demandé de modifier. Une méthode idempotente peut modifier l’état, mais la répéter a le même effet voulu que l’envoyer une seule fois : PUT (remplacer cette ressource par cette représentation) et DELETE sont idempotentes, comme toutes les méthodes sûres. POST n’est ni l’une ni l’autre, et PATCH n’est pas idempotente en général. Cette propriété compte parce que les réseaux tombent en panne entre le moment où une requête est traitée et celui où la réponse arrive. La RFC 9110 dit que les requêtes idempotentes peuvent être répétées automatiquement après une telle défaillance, et que les clients ne doivent pas rejouer automatiquement des requêtes non idempotentes, à moins de savoir que la requête peut en fait être répétée sans risque.

Les codes d’état font partie du contrat, car le code client se ramifie en fonction d’eux ; la section 0.6 a présenté les plus courants. Les codes de succès doivent dire ce qui s’est passé : 201 Created (avec un en-tête Location) quand une ressource est créée, 202 Accepted quand le travail a seulement été mis en file, 204 No Content pour un succès sans corps. Pour les conflits, 409 signifie que la requête est en contradiction avec l’état actuel de la ressource, 412 qu’une précondition a échoué et 422 qu’une requête bien formée est sémantiquement invalide. La règle architecturale porte sur les nouvelles tentatives : une réponse 4xx échouera de nouveau si la requête est renvoyée sans modification (429 Too Many Requests est l’exception : attendez le délai indiqué, puis réessayez), tandis qu’un délai d’expiration, un 502, un 503 ou un 504 peut réussir plus tard. Les clients ne doivent rejouer que ces cas-là, avec backoff et jitter, et seulement si la requête est idempotente ou porte une clé d’idempotence.

Deux autres fonctionnalités de HTTP résolvent à peu de frais des problèmes distribués. Les requêtes conditionnelles offrent un contrôle de concurrence optimiste : un client lit une commande avec son étiquette de version (ETag), puis envoie If-Match avec cette étiquette lors de la mise à jour ; si quelqu’un d’autre a modifié la commande entre-temps, le serveur répond 412 et rien n’est écrasé. Les en-têtes de cache (Cache-Control, ETag) permettent aux navigateurs, aux CDN et aux proxys de réutiliser les réponses, ce qui est souvent la tactique de performances la moins coûteuse qui soit.

Méthodes HTTP : sûres, idempotentes, ou ni l’un ni l’autre (RFC 9110 ; PATCH d’après la RFC 5789)

MéthodeUsage typiqueSûre ?Idempotente ?Rejouable à l’aveugle sans risque ?
GET, HEADLire une ressourceOuiOuiOui
PUTCréer ou remplacer à une URI connueNonOuiOui
DELETESupprimer une ressourceNonOuiOui (un second appel peut renvoyer 404, mais l’effet est le même)
POSTCréer dans une collection, ou exécuter une actionNonNonSeulement avec une clé d’idempotence ou un autre contrôle des doublons
PATCHMise à jour partielleNonPas en généralSeulement si le patch est écrit pour être idempotent, ou s’il est conditionné à une version

gRPC : le contrat d’abord, du binaire sur le réseau

gRPC est un framework d’appel de procédure à distance que Google a publié en open source en 2015 pour succéder à Stubby, le système RPC qu’il utilisait en interne depuis plus de dix ans ; la version 1.0 a suivi en 2016. Le contrat est écrit dans un fichier .proto à l’aide de Protocol Buffers, un format de sérialisation binaire compact, et des générateurs de code produisent des stubs clients typés et des squelettes de serveur dans de nombreux langages. Les appels passent par HTTP/2, qui multiplexe de nombreuses requêtes simultanées sur une seule connexion.

gRPC propose quatre sortes d’appels : unaire (une requête, une réponse), streaming serveur (une requête, un flux de réponses, comme les positions des livreurs en direct), streaming client (un flux de requêtes, une réponse, comme l’envoi de télémétrie) et streaming bidirectionnel. Les clients fixent une échéance (deadline) ; une fois celle-ci passée, l’appel se termine par DEADLINE_EXCEEDED, et les échéances peuvent être propagées le long d’une chaîne d’appels afin que le travail s’arrête partout dès que plus personne n’attend la réponse.

Ses points forts sont l’efficacité, le typage fort et le code généré, qui le rendent populaire pour les appels internes de service à service dans les organisations polyglottes. Ses coûts en sont le reflet. Les données binaires sont difficiles à lire dans les journaux et avec les outils ordinaires. Les navigateurs ne peuvent pas appeler gRPC directement : les API publiques et exposées au web ont donc généralement besoin d’un proxy ou d’une façade REST ou GraphQL. La mise en cache HTTP ne s’applique pas de la manière habituelle. La compatibilité dépend d’une discipline sur les numéros de champ : c’est le numéro d’un champ, et non son nom, qui l’identifie sur le réseau ; les numéros ne doivent donc jamais être réutilisés, et les champs supprimés doivent être marqués comme réservés.

syntax = "proto3";

service Dispatch {
  rpc AssignCourier (AssignRequest) returns (Assignment);
  rpc TrackCourier (TrackRequest) returns (stream Position);
}

message AssignRequest {
  string order_id = 1;
  string restaurant_id = 2;
  reserved 3;            // field removed in v2: never reuse 3
}

message Position {
  double lat = 1;
  double lng = 2;
  int64 recorded_at_ms = 3;
}
Un contrat gRPC pour le service d’affectation de la plateforme de livraison de repas, avec un appel unaire et un appel en streaming serveur (syntaxe Protocol Buffers).

GraphQL : le client choisit la forme

GraphQL est un langage de requête et un environnement d’exécution pour les API que Facebook a développé en interne à partir de 2012 pour ses applications mobiles, puis publié en 2015 avec une ébauche de spécification et une implémentation de référence ; il a été confié en 2018 à la GraphQL Foundation, neutre vis-à-vis des fournisseurs. Le serveur publie un schéma typé ; les clients envoient à un endpoint unique des requêtes qui nomment exactement les champs voulus, éventuellement à travers des objets liés, et reçoivent une réponse JSON de la même forme. Les mutations modifient les données, et les abonnements envoient des mises à jour.

Son principal avantage concerne les clients variés. Une application mobile de livraison de repas sur une connexion lente peut demander l’état d’une commande, le nom du restaurant et le prénom du livreur en un seul aller-retour, sans recevoir les vingt autres champs dont le tableau de bord web a besoin. Cela évite à la fois la récupération excessive de données (over-fetching) et la chaîne de requêtes qu’une API REST ressource par ressource peut exiger, et cela peut remplacer un backend distinct pour chaque frontend. Le schéma est en outre autodocumenté et permet aux outils de valider les requêtes avant leur exécution.

Les compromis sont réels et sont surtout supportés par l’équipe serveur. Chaque champ est rempli par un résolveur (resolver) : une implémentation naïve qui récupère séparément le restaurant de chaque commande produit le problème des requêtes N+1 : une requête pour cinquante commandes, puis cinquante de plus pour leurs restaurants. La solution habituelle est le regroupement des requêtes (batching) et un cache par requête, popularisés par la bibliothèque DataLoader. Comme les clients peuvent composer des requêtes arbitrairement profondes, les serveurs ont besoin de limites de profondeur et de coût des requêtes, de délais d’expiration et souvent d’une liste d’autorisation de requêtes approuvées (persistées) pour empêcher une surcharge accidentelle ou malveillante. La mise en cache HTTP est moins efficace quand tout est un POST vers une seule URL. L’autorisation doit être appliquée par champ ou par objet plutôt que par URL. Enfin, les erreurs sont généralement signalées dans un tableau errors à l’intérieur d’une réponse HTTP réussie, ce qui échappe à une supervision fondée sur les codes d’état.

Messagerie : files et topics

La messagerie déplace la conversation dans un courtier. Comme l’explique l’architecture pilotée par les événements, une file remet chaque message à l’un des consommateurs concurrents, la méthode standard pour faire monter en charge un pool de workers, tandis qu’un topic en remet une copie à chaque abonné : les services des restaurants, d’affectation et de notifications peuvent ainsi réagir chacun à OrderPlaced sans que le service des commandes sache qu’ils existent. Cette section-là traite aussi des garanties de livraison, de l’ordre, des doublons et des files de lettres mortes ; files d’attente et back pressure explique comment les files absorbent les pics de charge.

La messagerie peut aussi transporter des requêtes et des réponses : le demandeur envoie une commande avec un identifiant de corrélation et une adresse de réponse, puis associe la réponse, quand elle arrive, à sa requête. Cela conserve le découplage temporel d’un courtier tout en obtenant une réponse, au prix de pièces mobiles supplémentaires.

REST, gRPC, GraphQL et messagerie comparés

AspectREST sur HTTPgRPCGraphQLMessagerie (files et topics)
InteractionRequête-réponse synchroneSynchrone ; unaire ou en streamingRequêtes et mutations synchrones ; les abonnements poussent les donnéesAsynchrone ; fire-and-forget ou requête-réponse via des identifiants de corrélation
ContratDocument OpenAPI (facultatif mais courant)Fichiers .proto (obligatoires)Schéma (obligatoire)Document AsyncAPI et schémas de messages (facultatifs mais conseillés)
Charge utileGénéralement du texte JSONBinaire Protocol BuffersJSONTous : JSON, Avro, Protocol Buffers
Couplage temporelOuiOuiOuiNon : le courtier sert de tampon
Mise en cacheExcellente : caches HTTP standard et CDNPeu de choses intégréesPlus difficile : nécessite des requêtes persistées ou des caches côté clientSans objet
Prise en charge par les navigateursNativeNécessite un proxy (gRPC-Web)NativePas directement ; via un backend ou des WebSockets
Usage idéalAPI publiques et partenaires ; CRUD orienté ressourcesAppels internes entre services, à fort volume et faible latence ; streamingNombreux types de clients aux besoins de données différents ; agrégation de plusieurs servicesTravail qui peut être fait plus tard ; fan-out vers de nombreux consommateurs ; lissage de la charge
Principaux piègesAPI bavardes ; conventions incohérentesOpaque au débogage ; discipline sur les numéros de champRésolveurs N+1 ; coût des requêtes non borné ; autorisation au niveau des champsDoublons, ordre, flux invisibles, exploitation du courtier

Concevoir des API qui durent : ressources, pagination et filtrage

Modélisez les ressources autour du domaine, pas de la base de données. Exposez les noms avec lesquels vos consommateurs raisonnent (/orders, /orders/1042/items, /restaurants/7/menu), pas des tables ni des services internes. Les actions qui n’entrent pas dans les catégories créer, lire, mettre à jour et supprimer peuvent généralement être modélisées elles-mêmes comme des ressources : POST /orders/1042/cancellation crée une annulation, qui peut porter un motif et être refusée avec un 409 si le repas est déjà en route. Cela préserve l’interface uniforme tout en exprimant honnêtement le processus métier.

Paginez chaque collection, car les collections grandissent. La pagination par offset (?offset=40&limit=20) est simple et permet de sauter à la page 9, mais elle ralentit sur les pages lointaines, car la base de données doit sauter toutes les lignes précédentes, et elle affiche des doublons ou saute des éléments quand des lignes sont insérées pendant que quelqu’un parcourt les pages. La pagination par curseur renvoie un jeton opaque qui pointe juste après le dernier élément (?after=eyJpZCI6MTA0Mn0&limit=20) ; elle reste rapide et stable en cas d’insertions, au prix de l’absence d’accès direct. Pour les fils d’actualité et les données volumineuses ou changeantes, les curseurs sont le choix par défaut le plus sûr. Plafonnez toujours la taille des pages côté serveur.

Le filtrage et le tri doivent utiliser des paramètres de requête prévisibles (?status=delivered&placed_after=2026-10-01&sort=-placed_at) et s’appuyer sur des index ; une API qui laisse les clients filtrer sur n’importe quel champ invite des requêtes auxquelles la base de données ne peut pas répondre rapidement. Ne prenez en charge que les filtres dont les consommateurs ont besoin, et documentez-les dans le contrat.

Clés d’idempotence : rendre POST rejouable sans risque

La défaillance partielle (section 6.1) laisse un client qui n’a reçu aucune réponse dans l’incertitude : sa requête a-t-elle été traitée ? Rejouer un GET est sans danger ; rejouer POST /orders peut créer une seconde commande et débiter le client deux fois. Une clé d’idempotence transforme cette nouvelle tentative risquée en une nouvelle tentative sûre. Le client génère une valeur unique (généralement un UUID aléatoire) pour chaque opération logique et l’envoie dans un en-tête tel que Idempotency-Key. Le serveur enregistre la clé avec le résultat de la première requête ; toute requête ultérieure portant la même clé renvoie le résultat enregistré au lieu de refaire le travail.

Les prestataires de paiement ont popularisé cette technique, et le groupe de travail HTTP APIs de l’IETF a rédigé un projet d’en-tête Idempotency-Key standard. À l’heure où ces lignes sont écrites, il n’a pas été publié en tant que RFC (le dernier projet, d’octobre 2025, a expiré), mais ses règles reflètent la pratique courante : une clé réutilisée avec un corps de requête différent doit être rejetée avec un 422, une nouvelle tentative qui arrive pendant que la première requête est encore en cours doit recevoir un 409 Conflict afin que deux copies ne s’exécutent jamais en même temps, et les clés sont propres à un client et expirent après une durée publiée, souvent 24 heures. L’exemple commenté applique chacune de ces règles.

Une commande rejouée avec une clé d’idempotence

Diagramme de séquence : une requête de commande rejouée avec la même clé d’idempotence renvoie le résultat stocké au lieu de créer une seconde commandeApp clientAPIBase1. POST /orders clé : k1enregistre commande et clé k1validée : commande 1042réponse 201 perdueexpiré : relance2. POST /orders clé : k1cherche k1 : déjà traitéeréponse stockée201 Created : cde 1042
La première tentative réussit, mais sa réponse est perdue ; la nouvelle tentative porte la même clé, si bien que l’API renvoie le résultat stocké au lieu de créer la commande 1043.

Exemple commenté

Exemple commenté : le double appui au moment de payer

Un client dont la connexion mobile est faible appuie sur « Commander ». L’app génère la clé 7f3c… et envoie POST /v1/orders. Le service des commandes stocke la clé avec une empreinte (hash) du corps de la requête et, plus tard, la réponse.

Tentative 1La commande 1042 est créée et le paiement autorisé ; la clé, l’empreinte du corps et la réponse 201 sont enregistrées dans la même transaction de base de données que la commande. La réponse est perdue quand le téléphone passe du Wi-Fi aux données mobiles.
Tentative 2 (nouvelle tentative automatique, même clé)Le service trouve la clé avec le statut completed, vérifie que l’empreinte du corps correspond, et renvoie le 201 stocké pour la commande 1042. Pas de seconde commande, pas de second débit.
Tentative 3 (le client appuie de nouveau pendant que la tentative 2 est en cours)L’app réutilise la même clé pour le même panier. Si la première requête était encore en cours de traitement, le service répondrait 409, et l’app attendrait avant de réessayer.
Un bogue envoie la même clé avec un panier différentLes empreintes des corps diffèrent : le service répond 422 au lieu de renvoyer silencieusement la mauvaise commande.
Au bout de 24 heuresLa clé expire et est supprimée ; l’app ne rejoue jamais cet ancien panier.

La clé doit être générée une seule fois par opération logique (par soumission de panier), et non à chaque tentative HTTP ; une nouvelle clé à chaque nouvelle tentative va à l’encontre du but recherché.

C’est l’enregistrement de la clé dans la même transaction que la commande qui rend la vérification fiable : un plantage ne peut laisser ni une commande sans sa clé, ni une clé sans sa commande.

La même idée, appliquée aux messages plutôt qu’aux requêtes HTTP, est le consommateur idempotent de l’architecture pilotée par les événements.

def create_order(request, db):
    key = request.headers.get("Idempotency-Key")
    if not key:
        return problem(400, "missing-idempotency-key")
    body_hash = sha256(request.body)
    with db.transaction():
        row = db.find_idempotency(request.client_id, key)
        if row and row.body_hash != body_hash:
            return problem(422, "idempotency-key-reused")
        if row and row.status == "in_progress":
            return problem(409, "request-in-progress")
        if row:
            return row.saved_response          # replay, do nothing new
        db.insert_idempotency(request.client_id, key, body_hash, "in_progress")
        order = db.insert_order(request.json)
        response = created(order)
        db.complete_idempotency(request.client_id, key, response)
    return response
Traitement côté serveur d’une clé d’idempotence (simplifié : une transaction de base de données par requête ; la contrainte d’unicité sur la clé empêche deux requêtes simultanées de s’exécuter toutes les deux).

Des erreurs lisibles par les machines : la RFC 9457

Les clients ont besoin d’erreurs sur lesquelles ils peuvent agir par programme, et les humains d’erreurs qu’ils peuvent comprendre. Plutôt que de laisser chaque équipe inventer sa propre forme d’erreur, utilisez Problem Details for HTTP APIs, la RFC 9457 (juillet 2023), qui a remplacé la RFC 7807 (2016). Un objet problem details a le type de média application/problem+json et cinq membres standard : type (une URI qui identifie le type de problème et peut renvoyer vers de la documentation), title (un résumé court et lisible par un humain de ce type de problème), status (le code d’état HTTP), detail (une explication de cette occurrence) et instance (une URI qui identifie cette occurrence). Des membres d’extension peuvent porter des précisions lisibles par une machine, par exemple les champs qui n’ont pas passé la validation.

Deux règles rendent les réponses d’erreur sûres. Le code client doit se ramifier en fonction de type et de status, jamais du libellé de title ou de detail, qui peut être traduit ou amélioré. Et les erreurs ne doivent pas divulguer d’éléments internes : les traces de pile, les fragments SQL et les noms d’hôtes internes aident davantage les attaquants que les appelants. Journalisez les détails avec un identifiant de corrélation et renvoyez plutôt cet identifiant.

{
  "type": "https://api.fooddelivery.example/problems/outside-delivery-area",
  "title": "Address is outside the delivery area",
  "status": 422,
  "detail": "Restaurant 7 delivers within 4 km; the address is 6.2 km away.",
  "instance": "/v1/orders/attempts/9b1e",
  "max_distance_km": 4,
  "actual_distance_km": 6.2
}
Une réponse problem details (HTTP 422, Content-Type: application/problem+json) pour une commande hors de la zone de livraison.

Faire évoluer une API sans casser ses consommateurs

Une API qui a des consommateurs est une promesse, et la loi de Hyrum (section 0.6) avertit que quelqu’un dépend de chaque comportement observable. Le changement est donc régi par un seul principe : faites librement des changements rétrocompatibles et évitez ceux qui cassent la compatibilité. Les consommateurs y contribuent en étant des lecteurs tolérants (tolerant readers, un terme qu’emploie Martin Fowler) : ils ignorent les champs qu’ils ne comprennent pas et ne lisent que ce dont ils ont besoin, si bien que les ajouts ne les cassent jamais. Le tableau ci-dessous résume les règles habituelles ; la même logique, appliquée aux données stockées et aux schémas d’événements, apparaît dans l’évolution des schémas.

Quand un changement cassant est inévitable, versionnez. Les stratégies courantes sont une version dans le chemin de l’URL (/v2/orders : explicite, facile à router et à mettre en cache, mais qui encourage de grands sauts peu fréquents), un en-tête personnalisé ou un paramètre de type de média (Accept: application/vnd.fooddelivery.v2+json : garde les URL stables, mais est moins visible et plus difficile à tester dans un navigateur), et des versions datées fixées pour chaque compte client, que certains grands fournisseurs d’API utilisent pour publier en continu de petits changements cassants, chaque client faisant la mise à niveau quand il le décide. Les API internes entre services appartenant à la même organisation évitent souvent complètement les versions grâce à l’approche d’expansion-contraction (expand and contract) : ajouter le nouveau champ ou le nouvel endpoint, migrer chaque consommateur, puis supprimer l’ancien. Quelle que soit la stratégie, annoncez les dépréciations à l’avance ; les en-têtes de réponse Deprecation (RFC 9745, 2025) et Sunset (RFC 8594, 2019) permettent à un serveur d’indiquer aux clients, sous une forme lisible par une machine, qu’un endpoint est déprécié et quand il cessera de fonctionner.

Changements rétrocompatibles et changements cassants d’une API

ChangementCompatible ?Pourquoi
Ajouter un champ de requête ou un paramètre de requête facultatifOuiLes clients existants ne l’envoient pas ; le serveur doit appliquer une valeur par défaut raisonnable.
Ajouter un champ à une réponseEn généralLes lecteurs tolérants l’ignorent ; les clients stricts qui rejettent les champs inconnus casseront : documentez donc que les clients doivent tolérer les ajouts.
Ajouter un nouvel endpoint ou une nouvelle ressourceOuiPersonne ne l’appelle encore.
Ajouter une valeur à une énumération dans une réponseRisquéLes clients dotés d’instructions switch exhaustives peuvent échouer sur la valeur inconnue ; documentez que de nouvelles valeurs peuvent apparaître.
Rendre obligatoire un champ de requête facultatifNonLes clients existants qui l’omettent se mettent à échouer avec un 400.
Supprimer ou renommer un champ, ou changer son type ou ses unitésNonLes clients qui lisent l’ancien champ n’obtiennent rien, ou une valeur erronée.
Changer la signification d’un champ ou d’un code d’étatNonLa rupture la plus dangereuse : rien n’échoue visiblement, mais le comportement est faux.
Durcir la validation (longueur maximale plus courte, format plus strict)NonDes requêtes qui réussissaient échouent désormais.

Décrire les contrats : OpenAPI et AsyncAPI

Un contrat écrit dans un format lisible par une machine peut piloter la documentation, la génération de clients, les stubs serveur, les serveurs simulés (mock), le linting et les tests. OpenAPI est la norme pour les API HTTP. Elle est issue de la spécification Swagger, que SmartBear a donnée en 2015 à l’OpenAPI Initiative, nouvellement créée sous l’égide de la Linux Foundation, et qui a été renommée en 2016 ; la version 3.1 (2021) a aligné ses schémas sur JSON Schema, et la version 3.2 a suivi en septembre 2025. AsyncAPI applique la même idée aux API pilotées par messages : elle décrit les canaux (files et topics), les opérations qui y envoient ou y reçoivent des messages et les schémas de messages ; la version 3.0 est sortie en décembre 2023.

Les équipes choisissent entre l’approche design-first (rédiger et relire le contrat, puis l’implémenter) et l’approche code-first (générer le contrat à partir de code annoté). Le design-first fait de l’API une décision délibérée que les consommateurs peuvent examiner avant que quoi que ce soit ne soit construit, et convient aux API publiques et inter-équipes ; le code-first maintient le contrat et l’implémentation en phase avec moins d’effort, et convient aux API internes qui changent vite. Dans les deux cas, gardez le contrat en gestion de versions, passez-le au linter dans le pipeline et lancez une vérification des changements cassants qui compare chaque version proposée à la version publiée.

Tests de contrat et contrats pilotés par le consommateur

Les tests de bout en bout qui déploient tous les services ensemble sont lents, instables, et deviennent plus lents à mesure que les services se multiplient ; pourtant, les tests unitaires ne peuvent pas détecter qu’un fournisseur a modifié un champ dont dépend un consommateur. Les tests de contrat comblent ce manque : chaque partie est testée isolément par rapport à une description partagée de leurs interactions. Avec les contrats pilotés par le consommateur, un patron décrit par Ian Robinson en 2006, chaque consommateur déclare les parties de l’API du fournisseur qu’il utilise réellement, et le fournisseur exécute toutes les attentes de ses consommateurs dans son propre build. Le fournisseur sait alors exactement quels champs personne ne lit (et peut donc les modifier sans risque) et quels changements casseraient un consommateur nommément identifié.

Pact, créé chez realestate.com.au en 2013, est l’outil le plus connu. Les tests du consommateur s’exécutent contre un fournisseur simulé (mock) Pact et enregistrent les requêtes envoyées et les réponses attendues dans un fichier de contrat ; le pipeline du fournisseur rejoue ces requêtes contre le véritable fournisseur et vérifie les réponses. Un broker stocke les contrats et les résultats de vérification : avant de déployer, un pipeline peut ainsi demander si cette version d’un service est compatible avec les versions de ses consommateurs et de ses fournisseurs déjà en production. Les tests de contrat ne remplacent pas quelques tests de fumée (smoke tests) de bout en bout, et ils vérifient la structure plutôt que la signification métier, mais ils permettent de déployer les services indépendamment en toute confiance, ce qui est précisément l’intérêt de les avoir séparés.

Vérifiez votre compréhension

Pourquoi placer une file entre deux services ne supprime-t-il pas tout couplage entre eux ?

Elle supprime le couplage temporel, mais les deux services dépendent toujours du format et de la signification du message, ainsi que du courtier lui-même : une modification du message peut donc encore casser le destinataire.

Pourquoi DELETE est-elle idempotente alors qu’un second DELETE peut renvoyer 404 au lieu de 204 ?

L’idempotence concerne l’effet voulu sur le serveur, pas la réponse : après un ou deux DELETE, la ressource a disparu dans les deux cas.

Un client mobile génère une nouvelle clé d’idempotence à chaque nouvelle tentative. Qu’est-ce qui ne va pas ?

Le serveur voit chaque nouvelle tentative comme une nouvelle opération : une réponse perdue suivie d’une nouvelle tentative peut donc encore créer une commande en double ; la clé doit appartenir à l’opération logique, pas à la tentative HTTP.

Pourquoi la pagination par curseur peut-elle afficher une liste stable quand de nouvelles commandes arrivent pendant que l’utilisateur parcourt les pages, alors que la pagination par offset peut ne pas le faire ?

Le curseur pointe juste après un élément précis : des insertions ailleurs ne décalent donc pas la page suivante ; un offset compte des lignes : de nouvelles lignes font passer des éléments d’une page à l’autre, ce qui provoque des doublons ou des éléments sautés.

Suivant · 6.6CAP et cohérenceCe que signifie la cohérence pour des données répliquées : les modèles, de la linéarisabilité à la cohérence à terme, ce que le théorème CAP dit réellement et ce qu’il ne dit pas, le compromis quotidien sur la latence mis en évidence par PACELC, l’arithmétique des quorums et le choix de la cohérence selon le cas d’usage.Continuer →

Sources et lectures complémentaires

Une question sur cette section ? ArchitectBot, l’assistant d’étude IA du cours, répond d’abord à partir du cours.

Demander : « Notre service des commandes appelle de façon synchrone les services de paiement, des restaurants et d’affectation. Lesquels de ces appels devraient devenir asynchrones, et comment tenir le client informé ? »