Guide d'architecture logicielle

Conception d'API fiable : délais, idempotence, cache et passage à l'échelle

Les API fiables rendent explicites leur comportement de temps, cohérence, capacité et défaillance afin que les appelants réussissent sans submerger les systèmes qui les soutiennent.

Publié le
Mis à jour le
Temps de lecture
18 min de lecture
Sur cette page

Un contrat d'API ne se limite pas à un chemin, une méthode et un schéma JSON. Il définit aussi combien de temps un appelant doit attendre, si une opération peut être répétée, quelles réponses peuvent être mises en cache, comment la capacité est partagée et ce qu'un client doit faire lorsqu'une dépendance n'est pas disponible. Ces choix déterminent si une API reste utile lorsque le trafic ou les défaillances sont réels plutôt qu'idéaux. La conception d'API fiable, c'est précisément ce qui transforme un schéma en système dont d'autres équipes peuvent réellement dépendre.

Ce guide se concentre sur des contrôles neutres vis-à-vis des fournisseurs qui fonctionnent avec une passerelle, une périphérie managée, un service mesh ou un déploiement direct de service. Les noms de produits peuvent changer ; les exigences architecturales ne le devraient pas. Cette neutralité compte surtout à l'échelle où opère une couche d'orchestration managée : quand la même API est exposée derrière plus d'une passerelle, d'un CDN ou d'une région, le comportement de délai, d'idempotence, de cache et de limitation de débit doit rester cohérent partout où la requête peut atterrir, pas seulement à l'origine que l'équipe a sous les yeux.

Une réponse HTTP réussie n'est pas la seule condition de succès

Une API peut répondre rapidement tout en manquant à son contrat si elle expose les données d'un autre tenant, accepte un travail qu'elle ne peut pas achever, répète un prélèvement ou sert une représentation obsolète là où la fraîcheur est requise. Concevez ensemble le temps de réponse, la correction et la sécurité. Si plus d'un fournisseur ou d'une région peut accepter la même requête, vérifiez que les décisions d'idempotence, de limitation de débit et de cache concordent partout — un plan d'observabilité unique comme MYO rend visible cette dérive entre fournisseurs avant qu'elle n'atteigne un client sous la forme d'une commande dupliquée ou d'un 429 incohérent.

Scénario fil rouge : la commande Acme Shop

Prenons un scénario concret. Lors du lancement d'une vente flash, un client mobile peut perdre la réponse d'une requête POST /v1/orders pourtant déjà acceptée par le serveur — coupure réseau, application mise en arrière-plan, ou simple délai d'expiration côté client. Acme Shop ne doit pas créer une seconde commande lorsque ce client retente son envoi, et elle ne doit pas non plus affirmer qu'un paiement ou une préparation de commande est achevé avant que son flux de travail asynchrone n'ait réellement atteint cet état. L'objectif est précis : accepter une commande exactement une fois depuis un client qui retente sa requête, exposer une progression asynchrone honnête, et rejeter la surcharge avant qu'elle n'épuise le service de commande — ce qui suppose une identité de tenant authentifiée, un enregistrement d'idempotence durable adossé à une contrainte d'unicité, un magasin d'état d'opération, une capacité de worker bornée, et des identifiants de trace qui traversent la passerelle et la file.

Concrètement, le client authentifié envoie une clé d'idempotence stable avec sa requête de création de commande. La passerelle valide la charge utile et applique une limite de débit sensible au tenant avant toute écriture : rejeter tôt un travail invalide ou abusif coûte bien moins cher que de le laisser atteindre l'origine. Une fois la requête admise, la commande, l'enregistrement d'idempotence et l'événement d'outbox sont validés ensemble dans une même frontière transactionnelle. Le serveur répond alors par un accusé de réception — pas encore une confirmation de commande — accompagné d'une URL d'opération authentifiée que le client peut interroger, tandis que des workers en file progressent la commande jusqu'à un état terminé, réussi ou en échec.

La conception d'API fiable commence par des contrats exploitables

La conception d'API fiable commence toujours par le contrat, avant l'implémentation. Documentez le but de chaque opération, son périmètre d'authentification, le schéma d'entrée, la représentation de réponse, le modèle de pagination, le format d'erreur, la limite de débit, l'attente de délai d'expiration et les recommandations de nouvelle tentative. Versionnez intentionnellement : les champs ajoutés sont généralement plus faciles à faire évoluer que les changements sémantiques d'un champ que les clients interprètent déjà.

Utilisez précisément la sémantique HTTP. GET et HEAD sont des opérations de récupération sûres ; PUT et DELETE sont censées être idempotentes, même si une implémentation doit toujours rendre les effets répétés sûrs. POST crée ou déclenche souvent du travail et nécessite une stratégie d'idempotence lorsque les clients peuvent réessayer. Renvoyez des codes de statut qui permettent aux appelants de distinguer une entrée invalide, une autorisation manquante, un conflit, une limitation de débit et une indisponibilité temporaire.

Pour les opérations longues, évitez de maintenir une connexion client ouverte sans résultat borné. Validez et acceptez durablement la requête, renvoyez un identifiant d'opération et un emplacement de statut clair, puis achevez le travail de manière asynchrone. Indiquez l'objectif de réalisation, la période de rétention, la sémantique d'annulation et si une requête dupliquée renvoie l'opération existante. Chez Acme Shop, la ressource de statut exige le même contexte de tenant authentifié que la commande elle-même : un en-tête d'autorisation absent renvoie 401, tandis qu'un appelant authentifié d'un autre tenant reçoit 404 plutôt qu'un signal révélant l'existence de l'opération d'un tiers. Les états accepted, processing, completed et failed restent distincts, et la ressource de statut porte systématiquement Cache-Control: no-store : un 202 n'est jamais une garantie de paiement ou de préparation effectivement achevés, seulement la preuve que la demande a été durablement enregistrée.

Budgétez le temps de l'appelant à la dépendance

Choisissez d'abord un objectif de latence côté utilisateur, puis remontez le chemin critique. Une cible API de 500 ms ne peut pas inclure de façon sûre 400 ms de travail de passerelle, une requête de base de données non bornée et deux tiers qui réessaient. Donnez une échéance à chaque requête et propagez le temps restant aux appels en aval.

Définissez des délais d'expiration plus courts plus bas dans la pile afin que l'appelant ait le temps d'utiliser un repli, de renvoyer une erreur ou d'annuler le travail. Une expiration n'est pas la preuve que le travail s'est arrêté : un serveur peut encore écrire après la déconnexion du client. Prenez en charge l'annulation lorsque le runtime et la dépendance le permettent, et rendez les écritures métier idempotentes afin qu'un résultat incertain puisse être rapproché de façon sûre.

Les nouvelles tentatives doivent être exceptionnelles, bornées et guidées par l'opération :

  • Ne réessayez que les défaillances transitoires et seulement tant qu'il reste du temps dans l'échéance de l'appelant.
  • Utilisez un backoff exponentiel avec gigue pour éviter des vagues de nouvelles tentatives synchronisées.
  • Ne réessayez pas les erreurs de validation ou d'autorisation.
  • Exigez une clé d'idempotence ou une identité de requête durable pour les opérations répétées de création, paiement, réservation et mutation.
  • Publiez Retry-After sur les réponses de limitation de débit ou de surcharge lorsqu'une fenêtre de nouvelle tentative pertinente existe.

Une seule instance cliente qui retente sa requête est bon marché ; toute une flotte de clients qui retentent en même temps transforme un incident isolé en un second incident empilé sur le premier. Les travaux d'AWS Builders' Library sur les délais d'expiration, les nouvelles tentatives et le backoff avec gigue le montrent bien : le backoff espace les tentatives d'un même client, mais seule la gigue empêche de nombreux clients de se synchroniser sur les mêmes instants de nouvelle tentative. Traitez le volume de nouvelles tentatives comme un budget plutôt que comme un filet de sécurité illimité : plafonnez la part du trafic total qui peut être constituée de nouvelles tentatives — un point de départ courant est 10 % du débit primaire — et coupez les nouvelles tentatives une fois ce budget épuisé. Sans ce garde-fou, une dépendance dégradée à 20 % d'erreurs peut voir son trafic de nouvelles tentatives doubler ou tripler la charge qu'elle subit, transformant une défaillance partielle en défaillance totale.

Les disjoncteurs et bulkheads peuvent contenir une dépendance défaillante, mais leur comportement doit être observable et testé. Un disjoncteur qui s'ouvre trop vite peut créer sa propre panne ; un disjoncteur qui ne s'ouvre jamais ne fait que signaler une défaillance lente. Séparez les pools de concurrence du trafic critique et facultatif afin qu'un rapport coûteux ne consomme pas les ressources nécessaires à la connexion ou au paiement.

Concevez l'idempotence d'API dans les changements d'état

Une clé d'idempotence associe une intention client à un résultat durable. Stockez la clé, le principal authentifié ou le tenant, l'empreinte de la requête, l'état courant et la réponse finale pendant une période de rétention documentée. Si la même clé arrive avec la même intention, renvoyez le résultat d'origine ou courant. Si elle arrive avec une charge utile différente, renvoyez un conflit plutôt que d'appliquer une mutation ambiguë. Le brouillon de l'en-tête Idempotency-Key du groupe de travail IETF HTTPAPI normalise précisément cette forme : le client envoie une clé unique par opération voulue, et le serveur ne doit jamais appliquer deux fois la même clé à une charge utile différente. Il s'agit encore d'un Internet-Draft plutôt que d'une RFC publiée ; traitez-le comme une convention de nommage et de sémantique vers laquelle converger, pas comme une dépendance à intégrer telle quelle.

L'idempotence d'API dépend autant de la manière dont la clé est générée que de la façon dont elle est stockée. Une clé générée en mémoire juste avant l'envoi est perdue si le client plante entre sa génération et la réception de la réponse — la nouvelle tentative utilise alors une nouvelle clé et produit un second résultat. Préférez des clés dérivées d'un état client durable, par exemple un order_id combiné à un compteur de tentative monotone, ou persistées en stockage local avant l'envoi. L'implémentation de Stripe est une référence utile : elle met en cache le code de statut et le corps de la première requête pour une clé donnée, succès ou échec, puis rejoue exactement ce résultat pour toute nouvelle tentative plutôt que de réévaluer la requête. Concevez votre propre magasin ainsi : capturez la réponse terminale une seule fois et servez-la telle quelle ensuite, avec une fenêtre de rétention nettement plus longue que le délai de nouvelle tentative client le plus lent et réaliste — une fenêtre trop courte transforme silencieusement une nouvelle tentative sûre en doublon.

N'utilisez pas un cache en mémoire à courte durée de vie comme seul magasin d'idempotence. Les processus redémarrent, les requêtes peuvent arriver sur différentes instances et les nouvelles tentatives différées peuvent survenir après l'expiration d'une entrée de cache. Une contrainte d'unicité de base de données, une transactional outbox ou un magasin durable dédié rend la frontière résiliente.

Pour les événements émis après une écriture, utilisez une outbox ou une remise atomique équivalente afin que le changement d'état et le message ne divergent pas silencieusement. Les consommateurs doivent aussi être idempotents, car une livraison au moins une fois peut produire des doublons. L'ordre doit être une garantie déclarée par clé, et non une hypothèse fondée sur l'heure d'arrivée.

Gardez un seul rédacteur par clé d'idempotence entre régions et fournisseurs

Un magasin d'idempotence mono-région est simple : une seule base de données, une seule contrainte d'unicité, une seule réponse claire à la question « cette clé a-t-elle déjà été vue ? ». Le mode de défaillance apparaît dès que l'API est exposée derrière plus d'un point d'entrée — une seconde passerelle, une région secondaire, un fournisseur de secours — et que chacun peut accepter des écritures. Si deux points d'entrée consultent chacun une réplique locale ou en cohérence à terme avant que l'écriture de l'autre n'ait eu le temps de se propager, les deux peuvent conclure que la clé est nouvelle et créer deux commandes. La contrainte d'unicité finit par se déclencher, mais seulement après que le dommage s'est propagé en aval — deux captures de paiement, deux événements de préparation.

Il n'existe pas de raccourci : l'idempotence devient un problème de consensus dès que plusieurs rédacteurs peuvent accepter la même clé. Acme Shop dispose des trois mêmes options que toute API à points d'entrée multiples : partitionner les clés vers une seule région ou un seul shard propriétaire par un routage déterministe, par exemple un hachage du tenant et de la clé d'idempotence, afin qu'un seul rédacteur puisse jamais accepter une clé donnée ; exiger une écriture fortement cohérente et adossée à un quorum pour l'enregistrement d'idempotence lui-même, même si le reste de la transaction reste en cohérence à terme ; ou accepter une fenêtre étroite de risque de doublon, la détecter et la réconcilier après coup via la même contrainte d'unicité complétée par une annulation compensatoire. La plupart des équipes retiennent la première option, qui rend le mode de défaillance local et testable plutôt que probabiliste.

Mettez en cache les lectures en sécurité à la périphérie et au niveau service

Le cache peut supprimer la latence d'origine et protéger la capacité amont, mais il ne doit pas affaiblir l'autorisation ou la fraîcheur. Classez une réponse avant de lui attribuer une politique de cache partagé :

Type de réponseValeur par défaut plus sûreNotes
Ressource publique et versionnéeCache partagé avec fraîcheur longueUtilisez des URL immuables ou un versionnage validé.
Catalogue public ou données de référenceCache partagé avec TTL explicite et validationDéfinissez l'invalidation et le comportement obsolète.
Réponse authentifiée, spécifique à un tenantPrivée ou no-store par défautNe comptez jamais sur un cache partagé pour déduire les frontières des tenants.
Autorisation, solde de compte, état de paiementAucun cache partagé sauf conception formelleLa correction et la confidentialité l'emportent sur un faible gain de latence.

Définissez la clé de cache délibérément. Elle peut inclure le chemin, des paramètres de requête sélectionnés, les en-têtes de représentation, la locale et une partition d'autorisation sûre. N'incluez pas indistinctement des en-têtes ou cookies non bornés, ce qui détruit l'efficacité du cache ; n'omettez pas les dimensions qui peuvent conduire à servir une représentation au mauvais appelant.

L'en-tête Vary est l'endroit où se cachent la plupart des erreurs de clé de cache. Selon la RFC 9111, un cache doit intégrer dans sa clé chaque en-tête nommé dans Vary ; une réponse qui varie selon Accept-Language ou un en-tête propre au tenant, mais qui omet de le déclarer dans Vary, peut être servie à la mauvaise locale ou, pire, au mauvais tenant dès qu'un cache partagé intervient. Vary: * est légal mais force une revalidation systématique, ce qui supprime discrètement le bénéfice du cache tout en ayant l'air correctement configuré. Traitez une déclaration Vary absente ou incomplète sur une réponse non uniforme pour tous les appelants comme un bug de correction, pas comme un détail de performance.

Les directives stale-while-revalidate et stale-if-error (RFC 5861) laissent un cache servir une réponse légèrement obsolète pendant qu'il revalide en arrière-plan, ou servir la dernière bonne réponse quand l'origine échoue, plutôt que de forcer chaque appelant à attendre une récupération synchrone. Elles conviennent bien à un catalogue public ou à des données de référence, où quelques secondes d'obsolescence sont un compromis acceptable pour absorber un incident d'origine — jamais à une réponse authentifiée, spécifique à un tenant ou financière, où « potentiellement obsolète » est précisément ce que la règle no-store ci-dessus vise à empêcher.

Utilisez le regroupement de requêtes pour les clés populaires afin d'empêcher une tempête d'échecs. Placez un origin shield ou une couche d'agrégation contrôlée derrière la périphérie lorsque cela convient à la topologie. Gardez l'origine privée, authentifiez le trafic edge-à-origine, ne validez les en-têtes transmis que depuis des proxys de confiance, et limitez les connexions et la concurrence de l'origine. Un CDN ne protège pas une origine que des attaquants peuvent atteindre directement.

Maîtrisez la charge grâce à la limitation de débit API avant la panne

La limitation de débit API alloue une ressource finie. Adaptez-la au risque : un endpoint non authentifié peut nécessiter une limite orientée IP ou réseau, tandis qu'une API authentifiée nécessite normalement des limites par tenant, identifiant, opération et coût. Publiez des en-têtes ou une documentation expliquant la fenêtre de quota et le comportement de réponse, sans révéler de détails de capacité interne facilitant les abus.

La limitation de débit API a longtemps été signalée par des en-têtes ad hoc et propres à chaque fournisseur — les variantes X-RateLimit-* diffèrent d'une API à l'autre par leurs unités et leur sémantique de réinitialisation. Le brouillon RateLimit et RateLimit-Policy du groupe de travail IETF HTTPAPI normalise cela : RateLimit-Policy annonce la forme du quota, et RateLimit indique ce qu'il en reste, si bien qu'un client bien conçu peut se limiter lui-même avant de déclencher un 429 plutôt que de découvrir la limite en l'épuisant. Ce brouillon n'est pas encore une RFC, mais il reflète la direction vers laquelle l'écosystème converge ; adopter ces noms d'en-têtes dès maintenant coûte peu, car c'est une couche strictement additive et neutre vis-à-vis des fournisseurs, posée au-dessus du limiteur interne qui applique réellement le quota.

La limitation de débit seule n'arrête pas les requêtes valides coûteuses. Combinez-la avec des limites de taille du corps, la validation de schéma, des plafonds de pagination, une complexité maximale de requête lorsque pertinent, des limites de concurrence par route et des files de travail pour les opérations asynchrones. Rejetez tôt le travail à la périphérie ou à la passerelle lorsque la décision ne requiert pas l'origine.

Lorsque la capacité est contrainte, priorisez. Réservez une part pour les contrôles de santé et les transactions critiques, dégradez les enrichissements facultatifs et renvoyez une réponse de surcharge rapide et documentée plutôt que de laisser tous les appelants attendre jusqu'à l'épuisement du pool entier. Testez ce comportement dans des conditions de concurrence réalistes, et pas seulement avec des tests fonctionnels à requête unique. Lorsque plusieurs passerelles ou régions peuvent accepter le même tenant, assurez-vous que leurs compteurs de quota s'accordent entre eux — sans cela, un même client voit un quota restant différent selon le point d'entrée qu'il atteint. Chez Acme Shop, la passerelle applique une admission de commande par tenant et réserve une petite part de concurrence pour le paiement : quand la transaction de commande durable ou l'arriéré de workers atteint sa limite, l'API renvoie 503 avec un Retry-After, seulement si une nouvelle tentative ultérieure est sûre et utile — elle ne met jamais en file un nombre non borné de tentatives de paiement, ni ne confirme une commande avant que l'intention ne soit durablement enregistrée.

Validez, récupérez et diagnostiquez

Dans un tenant de test ou un canary validé, soumettez une commande, interrompez volontairement la réponse côté client, puis retentez exactement la même requête avec la même clé d'idempotence. La validation positive donne une seule commande et un seul identifiant d'opération. La validation négative consiste à modifier la charge utile en conservant la même clé : la réponse doit être un 409, sans seconde commande créée. La validation de défaillance simule un plantage de worker après l'écriture de l'outbox : le statut peut rester processing, mais la reprise doit publier ou réconcilier le même événement de commande sans dupliquer ni le paiement ni la commande.

Lorsque plus d'un point d'entrée peut accepter la même route de commande, ajoutez une quatrième passe : soumettez la requête initiale via une région ou une passerelle, puis déclenchez la nouvelle tentative avec la même clé via une autre, avant que la première écriture n'ait eu le temps de se répliquer. Une implémentation correcte se résout tout de même en une seule commande, car le partitionnement ou l'écriture par quorum décrits plus haut forcent les deux points d'entrée à s'accorder sur le même rédacteur propriétaire. Si ce test produit deux commandes, c'est la preuve que la frontière d'idempotence est locale à chaque région plutôt que globale — pas la preuve d'une course rare, mais un défaut de conception à corriger au niveau du routage ou de la garantie de cohérence, pas à masquer avec du backoff.

En cas de défaut de contrat ou de déploiement, arrêtez le nouveau trafic uniquement via le contrôle d'admission documenté de la route concernée, faites revenir en arrière le déploiement versionné, et préservez les enregistrements d'idempotence et d'outbox. Reprenez les workers progressivement une fois la dépendance rétablie, et réconciliez les effets externes incertains via les identifiants d'idempotence côté fournisseur. Ne supprimez pas les enregistrements d'idempotence, ne retentez pas les paiements en masse et ne rendez pas le statut public pour accélérer la reprise.

Quelques symptômes reviennent souvent en production :

  • Un client signale une commande dupliquée. Le même tenant et la même clé correspondent à plusieurs identifiants de commande. Désactivez la route de création concernée, examinez la frontière d'unicité et réconciliez avant de la rouvrir ; la reprise se confirme quand une même clé se résout en une seule commande et une seule opération dans un test de nouvelle tentative contrôlé.
  • Des opérations 202 ne se terminent jamais. L'âge de la file augmente ou des workers échouent sur une dépendance. Limitez l'admission, restaurez la dépendance ou le déploiement de worker en défaut, puis vérifiez que l'âge de complétion redescend sans pic d'erreurs de livraison.
  • Un 409 survient sur une nouvelle tentative pourtant identique. Les empreintes diffèrent après normalisation, ou un client a changé un champ. Renvoyez le conflit documenté, examinez la canonicalisation sans appliquer la requête ; la nouvelle tentative canonique doit alors renvoyer l'opération d'origine.
  • L'origine sature malgré les limites d'edge. Les appels de commande en vol ou l'attente du pool de base de données atteignent leur limite. Resserrez la concurrence par route, délestez les lectures non critiques et enquêtez sur la ressource contraignante jusqu'à ce que l'attente de pool, le taux de 503 et le taux d'acceptation des commandes reviennent à la cible.

Observez le comportement de la périphérie au magasin de données

Adoptez un modèle de corrélation et de traçage qui traverse la périphérie, la passerelle, l'application, les files et les dépendances. OpenTelemetry fournit des conventions neutres vis-à-vis des fournisseurs pour la télémétrie HTTP, RPC, de base de données et de messagerie. Enregistrez les modèles de route plutôt que les chemins bruts à forte cardinalité ; incluez l'état du cache, la décision de limitation de débit, la tentative de nouvelle tentative, la source d'expiration, l'attente en file et le résultat de dépendance.

Créez des indicateurs de niveau de service pour la disponibilité, la latence et la correction. Un tableau de bord API utile montre les durées p50, p95 et p99 ; les classes de réponse ; la saturation ; le travail rejeté ; le taux de hit du cache ; l'âge de la file ; et la latence des dépendances. Segmentez par opération, région, version de déploiement et type de client. Masquez les identifiants, en-têtes d'autorisation, données personnelles et corps de requête complets dans la télémétrie normale.

Faites délibérément les choix multi-régions et multi-fournisseurs

Le routage mondial peut rapprocher les appelants d'une périphérie, mais il ne peut pas éliminer le coût de cohérence d'une écriture qui doit atteindre une autorité distante. Gardez une propriété d'écriture claire, placez les réplicas de lecture selon l'obsolescence tolérée et exposez une version de réponse ou un horodatage lorsque les clients doivent raisonner sur la fraîcheur.

L'utilisation de plusieurs passerelles, CDN ou régions cloud peut réduire une dépendance opérationnelle unique. Elle exige également des politiques TLS, API, clés de cache, observabilité, critères de santé de routage et basculement testés cohérents. Construisez sur des interfaces HTTP, DNS, traçage et infrastructure portables, puis documentez les limites spécifiques au fournisseur comme contraintes de déploiement plutôt que de les intégrer dans le contrat API.

Liste de contrôle pour une API fiable

Avant d'exposer une API, vérifiez que les appelants reçoivent des recommandations de délai d'expiration et de nouvelle tentative ; que les mutations ont une idempotence durable ; que les politiques de cache correspondent à la sensibilité des données ; que l'accès à l'origine est restreint ; que les limites de débit, taille, pagination et concurrence sont explicites ; que les réponses de surcharge sont testées ; que les points d'entrée multi-régions ou multi-fournisseurs s'accordent sur l'état d'idempotence et de quota ; et que la télémétrie relie une décision edge aux résultats applicatifs et de dépendance. La conception d'API fiable n'est jamais terminée au lancement : traitez cette liste comme une revue récurrente à mesure que le trafic, les régions et les fournisseurs évoluent.

Références faisant autorité

Faites de la conception d'API fiable une discipline managée

Échangez avec Optimi sur l'orchestration cohérente de l'idempotence d'API, de la limitation de débit API, du cache et de la protection de l'origine entre vos fournisseurs, avec Performance, Sécurité et Visibilité unifiées dans MYO.

Discuter de l'architecture API