Ce que sont les microservices, et ce qu’ils ne sont pas
Les microservices sont un style architectural dans lequel une application est composée de services déployables indépendamment, organisés autour de capacités métier, chacun possédant ses propres données et communiquant via le réseau. Le mot clé ici est indépendamment. Un service qui ne peut pas être déployé seul n’est pas un microservice ; c’est un module avec une frontière réseau.
Il est important de préciser ce que ce style n’est pas. Ce n’est pas une règle stipulant que les services doivent être petits. Ce n’est pas une exigence d’utiliser des containers, Kubernetes ou un service mesh, bien que ces outils existent car ce style est complexe sans eux. Ce n’est pas automatiquement plus scalable, plus fiable ou plus moderne qu’un monolithe. Ce sont des propriétés que vous construisez, et non des propriétés intrinsèques à une topologie.
Le seul trait distinctif est qu’une modification apportée à un service peut atteindre la production sans nécessiter une release coordonnée des autres. Tout le reste — les frontières, les contrats, les événements, la plateforme — existe pour rendre ce trait possible et pour en gérer les conséquences.
Exemple de décomposition
Prenons une boutique en ligne. Les capacités sont faciles à identifier, et chacune devient un service avec un responsable clair.
catalog products, prices, availability
cart the pre-purchase basket
orders the placed order and its lifecycle
inventory stock levels and reservations
payments charges, refunds, payment methods
shipping labels, carriers, tracking
notifications email, push, SMS
identity accounts, sessions, API keys
Remarquez ce qui manque dans cette liste. Il n’y a pas de “service de base de données”, pas de “service d’envoi d’emails” — les notifications sont une capacité, pas un transport — et pas de “service utilisateur” que tous les autres services appellent simplement pour afficher un nom. Chaque entrée possède une partie du métier, et une équipe peut en gérer une ou deux de bout en bout.
Les interactions sont aussi importantes que la liste elle-même. Parcourir le catalogue est une opération lourde en lecture et peut être mise en cache agressivement. Passer une commande est une écriture qui coordonne l’inventaire, les paiements et l’expédition. Les notifications sont un pur consommateur : elles réagissent aux événements et ne sont jamais appelées. Ces différentes formes justifient des traitements différents — mise en cache, sagas, files d’attente — même si tous sont qualifiés de “services”.
Le véritable moteur est la déployabilité indépendante
Les équipes adoptent souvent les microservices « pour passer à l’échelle », pour s’apercevoir ensuite qu’elles n’en ont jamais eu besoin. La véritable raison d’un découpage est organisationnelle. Lorsque dix ingénieurs travaillent sur un seul déploiable, chaque mise en production attend la modification la plus lente, et un test qui échoue dans un domaine bloque tout le monde. Le découpage par capacité permet à chaque équipe d’avoir son propre pipeline, son propre système d’astreinte et son propre rythme.
C’est la loi de Conway à l’envers : vous façonnez le système pour que ses jointures correspondent à la manière dont vos équipes communiquent. Si une seule équipe possède l’intégralité du produit, les microservices vous apportent des pannes réseau et une plateforme à maintenir en échange de rien. Si six équipes doivent livrer indépendamment, les jointures commencent à devenir rentables.
La première question n’est donc pas « comment décomposer ce domaine ? », mais « quelles parties de ce système doivent réellement évoluer à des rythmes différents et être gérées par des personnes différentes ? ». Seules les réponses à cette question deviennent des services.
Les frontières suivent les capacités métier
La bonne couture est une capacité métier, et non une couche technique. La facturation, l’expédition, le catalogue, l’identité et les notifications sont des capacités. Un « service de base de données », un « service d’email » ou un « service de validation » est une couche technique déguisée en service, et chaque nouvelle fonctionnalité obligera à en modifier trois simultanément.
Le Domain-driven design fournit le vocabulaire nécessaire. Un bounded context est une frontière à l’intérieur de laquelle un modèle particulier et son langage sont cohérents. Une « Commande » dans le contexte des ventes désigne un panier sur le point d’être payé ; dans le contexte de l’exécution, elle désigne un colis à préparer et à expédier. Les deux sont corrects dans leur propre contexte, et forcer l’utilisation d’une classe Order partagée entre les deux est précisément ainsi qu’un monolithe devient un chaos.
Une bonne frontière possède trois propriétés :
- Elle détient un ensemble cohérent de règles qui évoluent ensemble.
- Elle possède une interface restreinte et stable par rapport à la quantité de logique qu’elle encapsule.
- Elle peut être comprise par une seule équipe sans avoir à lire le reste du système.
Les frontières sont coûteuses à déplacer une fois que d’autres services en dépendent, privilégiez donc des services moins nombreux et plus larges au début. Diviser un service est facile ; fusionner deux services qui ont divergé est douloureux.
Communication synchrone : REST et gRPC
L’interaction la plus simple est le couple requête-réponse. Utilisez REST avec JSON pour tout ce qui est accessible via un navigateur ou un client externe, et gRPC lorsque les deux parties sont internes et que vous souhaitez un contrat typé, compact et à faible latence. Les clients générés par gRPC rendent difficile tout écart par rapport au schéma, c’est pourquoi de nombreux maillages de services (service meshes) internes le standardisent.
Les appels synchrones sont faciles à appréhender, mais impossibles à rendre totalement sûrs. Deux services sont désormais temporellement couplés : l’appelant ne peut pas continuer tant que l’appelé n’est pas opérationnel et réactif. Trois règles permettent d’éviter que cela ne se transforme en panne générale.
- Définissez toujours un timeout. Un appel sans date limite peut rester suspendu jusqu’à l’épuisement de vos propres ressources. Choisissez un budget qui respecte la date limite de l’appelant et soustrayez le reste de son travail.
- Ne rejouez que les opérations idempotentes. Un
GETrejoué est sans danger ; unPOST /chargerejoué entraîne un second débit, à moins que le point de terminaison n’accepte et ne respecte une clé d’idempotence. - Coupez le circuit. Lorsqu’une dépendance échoue, arrêtez de l’appeler pendant un certain temps au lieu de mettre en file d’attente des requêtes qui échoueront également. C’est le principe du circuit breaker, qui transforme une dépendance lente en une erreur rapide et explicite.
Les tentatives de rejeu (retries) amplifient la charge. Si trois couches rejouent chacune l’appel trois fois, un service en difficulté reçoit vingt-sept requêtes pour chaque requête originale. Limitez les retries à la périphérie (edge) où vous avez une vue d’ensemble, et ajoutez du jitter pour éviter que les retries n’arrivent sous forme de vague synchronisée.
Communication asynchrone : les événements
L’alternative à l’interrogation est l’annonce. Un service publie un fait — orders.created, payment.captured — et un nombre indéterminé de consommateurs y réagissent sans que le producteur ne sache qu’ils existent. Cela élimine le couplage temporel : le service de paiement peut être hors ligne et la commande peut tout de même être créée, car l’événement attend sur le broker.
La communication asynchrone apporte résilience et flexibilité, au prix de l’immédiateté et de la clarté. La réponse du producteur n’inclut plus l’effet en aval, le système est donc éventuellement cohérent (eventually consistent). Tracer une requête revient alors à suivre une piste d’événements plutôt qu’une seule pile d’appels (call stack). De plus, le broker devient une infrastructure critique qui doit être durable et surveillée.
Il existe deux manières de coordonner un processus en plusieurs étapes. Dans la chorégraphie, chaque service écoute les événements et décide de la marche à suivre ; il n’y a pas de cerveau central, ce qui est élégant jusqu’au moment où plus personne n’est capable d’expliquer le flux global. Dans l’orchestration, un gestionnaire de processus ou un coordinateur de saga pilote explicitement les étapes. La chorégraphie est préférable pour les réactions simples, l’orchestration pour les flux impliquant des compensations et des timeouts. Les guides sur l’Event-Driven Architecture et Kafka approfondissent ces mécanismes.
La taxe des systèmes distribués
Dès qu’un appel traverse un réseau, un ensemble de modes de défaillance qui n’existent pas au sein d’un même processus devient votre responsabilité. Ce n’est pas une raison pour éviter les services ; c’est simplement le prix à payer pour les utiliser.
- Le réseau n’est pas fiable. Des paquets sont perdus, le DNS renvoie des adresses obsolètes, des connexions sont réinitialisées en plein milieu d’une requête.
- La panne est partielle. Le service A est opérationnel alors que le service B est hors service. Il n’existe pas de flag unique indiquant que « le système est en ligne ».
- La latence a un coût. Un appel qui prenait quelques microsecondes en local prend désormais plusieurs millisecondes, et les chaînes d’appels multiplient cet effet.
- La réponse peut être perdue. Une requête peut réussir, mais l’accusé de réception n’arrive jamais. Du point de vue de l’appelant, l’opération a échoué, alors que le travail a tout de même été effectué.
- Le temps n’est pas synchronisé. Les horloges divergent d’une machine à l’autre ; ordonner des événements uniquement par timestamp est donc risqué.
La réponse architecturale consiste en une boîte à outils restreinte et bien connue : des timeouts sur chaque appel, des tentatives de reconnexion limitées avec backoff et jitter, des circuit breakers, des bulkheads pour isoler la panne d’une dépendance, et des clés d’idempotence pour garantir que la répétition d’une requête est sans danger. Aucun de ces éléments n’est optionnel. Un système de microservices sans ces mécanismes est un système qui fonctionne… jusqu’au premier jour difficile.
Propriété des données et le piège de la base de données partagée
Chaque service est propriétaire de ses données. Cela signifie qu’un seul service a le droit d’écrire dans son schéma, et qu’aucun autre service ne se connecte à cette base de données. C’est cette propriété qui rend le déploiement indépendant possible, car une modification du schéma n’affecte que le service propriétaire et son contrat.
Une base de données partagée semble pratique, mais elle détruit insidieusement l’architecture. Deux services lisant les mêmes tables sont couplés via le schéma : renommez une colonne et les deux doivent être déployés. Pire encore, le schéma partagé devient de fait un modèle partagé, et les bounded contexts s’effondrent pour n’en former plus qu’un seul.
Les requêtes transversales entre services sont la raison habituelle pour laquelle les équipes se tournent vers une base de données partagée. Les solutions sont les read models et les API :
- Interrogez le service propriétaire via son API pour obtenir une réponse concise et spécifique.
- Abonnez-vous à ses événements et maintenez une projection locale adaptée à vos requêtes.
- Acceptez que certaines requêtes nécessitent un store de reporting dédié, et non une jointure entre les bases de données de différents services.
Une projection est dénormalisée et offre une cohérence éventuelle (eventual consistency), ce qui explique précisément pourquoi elle est rapide et pourquoi vous devez accepter un léger délai de synchronisation. C’est le côté lecture du CQRS, et c’est la méthode standard pour répondre à des questions transversales sans briser la propriété des données.
Les Sagas plutôt que les transactions distribuées
Il n’existe pas de transaction ACID s’étendant sur plusieurs services. Le commit à deux phases (two-phase commit) exige que tous les participants détiennent des verrous et soient disponibles, ce qui est précisément la condition que vous ne pouvez pas garantir à travers un réseau, et la plupart des bases de données modernes ne le supportent pas. La solution de remplacement est la saga.
Une saga est une séquence de transactions locales, une par service, où chaque étape publie un événement ou invoque la suivante. Si une étape ultérieure échoue, la saga exécute des actions de compensation pour annuler le travail accompli d’un point de vue métier. Prenons l’exemple de la passation d’une commande :
- Orders crée la commande en tant que
pending. - Inventory réserve le stock.
- Payments débite le client.
- Fulfilment planifie l’expédition et orders marque la commande
confirmed.
Si le paiement échoue, la compensation libère la réservation et annule la commande. Notez que l’« annulation » n’est pas un rollback ; c’est un nouveau fait métier. Vous ne pouvez pas « dés-envoyer » un e-mail de confirmation, donc la compensation consiste en un second e-mail expliquant l’annulation.
Comme chaque étape peut être retentée, chaque étape doit être idempotente. Une même commande ne doit pas être facturée deux fois. Dérivez une clé de déduplication à partir de l’opération métier — l’ID de la commande — et non à partir d’un ID de requête aléatoire, et laissez une contrainte d’unicité ou un SET NX rendre la vérification atomique. Les sagas sont la partie la plus exigeante des microservices, et négliger le chemin de compensation est la raison pour laquelle les systèmes se retrouvent avec des réservations orphelines et des doubles facturations.
Contrats, versionnage et compatibilité
Le contrat d’un service est son API, et les appelants en dépendent. Traitez-le comme une interface publiée avec son propre cycle de vie :
- Ajoutez, ne cassez pas. L’ajout de nouveaux champs optionnels est sans risque ; renommer, supprimer ou modifier la signification d’un champ ne l’est pas.
- Versionnez délibérément. Le versionnage via l’URL ou les headers rend les changements majeurs (breaking changes) explicites. Les guides sur le REST et le versionnage d’API détaillent les compromis à faire.
- Laissez du temps aux consommateurs. Annoncez les dépréciations, suivez les métriques d’utilisation par client, et ne supprimez un endpoint que lorsqu’il n’est plus utilisé.
- Testez le contrat des deux côtés. Les tests de contrat pilotés par le consommateur (consumer-driven contract tests) permettent de détecter les cas où un changement jugé « compatible » par le producteur ne l’est pas pour un consommateur spécifique.
La même discipline s’applique aux schémas d’événements. Un consommateur lisant un topic verra des messages écrits par une version plus ancienne du producteur ; les événements doivent donc être additifs et auto-descriptifs. Intégrez une version ou un identifiant de schéma dans l’enveloppe, et permettez aux consommateurs de tolérer les champs inconnus.
Découverte, passerelles et équilibrage de charge
Les services bougent. Les instances sont reprogrammées, mises à l’échelle et remplacées ; les appelants ne peuvent donc pas coder les adresses en dur. La découverte de services (service discovery) résout ce problème : soit le client interroge un registre pour trouver des instances saines (côté client), soit une adresse virtuelle stable placée devant les instances s’en charge (côté serveur). Kubernetes vous offre cette seconde option gratuitement via les Services et le DNS.
Une API gateway se situe à la périphérie et gère les problématiques que chaque service devrait autrement répéter : l’authentification, la limitation du débit (rate limiting), la terminaison TLS, le routage des requêtes et l’agrégation des réponses. C’est un outil réellement utile. Cependant, elle devient également un point de défaillance unique et, si elle accumule de la logique métier, un monolithe distribué en miniature. Gardez votre gateway légère et repoussez les règles spécifiques aux fonctionnalités vers le service concerné.
L’équilibrage de charge ne se résume pas au round-robin. Les connexions longue durée, les sessions persistantes (sticky sessions) et les consommateurs lents influencent tous la répartition du trafic. Laissez le load balancer de la plateforme faire son travail et rendez vos services stateless afin que n’importe quelle instance puisse répondre à n’importe quelle requête.
Observabilité entre les services
Dans un monolithe, une stack trace suffit généralement à expliquer un incident. Entre plusieurs services, la requête a quitté votre processus il y a plusieurs bonds et la trace est perdue. Trois instruments permettent de la rétablir.
- Les identifiants de corrélation (Correlation ids). Générez un id à l’entrée (edge), propagez-le dans les headers et les enveloppes d’événements, et insérez-le dans chaque ligne de log. C’est le fil conducteur qui lie l’action d’un utilisateur à chaque service sollicité.
- Le traçage distribué (Distributed tracing). Les spans OpenTelemetry enregistrent le temps passé dans chaque service et chaque appel. Un trace permet de voir d’un coup d’œil quel bond est lent, une question à laquelle les logs ne peuvent pas répondre.
- Les métriques par service. Suivez le taux de requêtes, le taux d’erreur et la durée pour chaque endpoint, ainsi que les signaux de saturation comme la profondeur des files d’attente (queue depth) et l’utilisation des pools. Configurez des alertes sur les symptômes ressentis par vos utilisateurs, et non sur chaque micro-variation.
Les logs doivent être au format JSON structuré, inclure le nom du service ainsi que l’identifiant de corrélation, et être centralisés en un seul endroit. Un log que vous ne pouvez pas rechercher à travers plusieurs services est un log que vous n’utiliserez pas à 3 heures du matin. Le guide sur le Déploiement Cloud couvre l’aspect opérationnel de la collecte de ces signaux.
Deadlines et budgets de requête
La latence dans une chaîne d’appels est additive, et chaque service de la chaîne avance à l’aveugle à moins que vous ne propagiez une deadline. Une requête orientée utilisateur qui doit répondre en 800ms ne peut pas se permettre quatre appels en aval acceptant chacun d’attendre deux secondes.
Attribuez un budget à chaque requête dès l’entrée (edge) et transmettez le temps restant lors de chaque appel. Chaque service soustrait le temps consacré à son propre traitement et transmet une deadline réduite, afin que la chaîne échoue rapidement au lieu d’empiler les timeouts.
// The gateway sets the budget once.
const deadline = Date.now() + 800;
await orders.place(input, { deadline }); // forwards the remaining time
Lorsqu’un service constate que la deadline est dépassée, il doit s’arrêter et renvoyer une erreur explicite plutôt que de lancer un travail supplémentaire qu’il ne pourra pas terminer. Couplez cela avec un fallback pour les appels non essentiels : si le service de recommandations ne peut pas répondre en 100ms, renvoyez la page sans elles. Un budget de requête transforme le “parfois ça freeze” en un p99 prévisible, et c’est l’un des gains de fiabilité les moins coûteux et les plus efficaces entre services.
Déploiement et infrastructure
Le déploiement indépendant est une propriété de votre pipeline, et pas seulement de votre topologie. Chaque service nécessite son propre processus de build, de test, d’image et de rollout, ainsi qu’un moyen d’exécuter des migrations de base de données sans interruption de service et un rollback qui ne dépend pas du redéploiement de tout le reste.
Cela implique généralement l’utilisation de containers et d’un orchestrateur. Les containers rendent l’environnement d’exécution du service portable et ses dépendances explicites ; Kubernetes ou un équivalent managé gère l’ordonnancement, la découverte, les health checks, l’autoscaling et les rolling updates. Vous avez également besoin d’une gestion de la configuration et des secrets par environnement, ainsi qu’une stratégie de migration de schéma qui reste compatible avec la version précédente du code, car les anciennes et nouvelles instances coexistent pendant un rollout.
C’est ce qu’on appelle la « taxe plateforme ». Elle est bien réelle, elle est permanente, et c’est pourquoi les petites équipes doivent mûrement réfléchir avant d’adopter ce style d’architecture.
Le monolithe distribué
Le monolithe distribué est le pire scénario possible : de nombreux services qui doivent pourtant toujours être déployés ensemble. Ses symptômes sont sans équivoque.
- Les services partagent une base de données, donc les modifications de schéma impactent plusieurs équipes.
- Une mise en production nécessite la coordination simultanée de plusieurs services.
- Une chaîne d’appels synchrones traverse quatre services pour une seule action utilisateur.
- Deux services sont systématiquement modifiés dans la même pull request.
Lorsque vous observez cela, c’est que les frontières sont mal définies. Les causes habituelles sont un découpage par couche technique, un découpage effectué avant d’avoir bien compris le domaine, ou le maintien d’une base de données partagée après la séparation. La solution consiste souvent à fusionner à nouveau les services concernés et à trouver un point de rupture plus pertinent ; c’est un aveu difficile à faire, mais c’est moins coûteux qu’une décennie de déploiements coordonnés.
Quand ne pas utiliser les microservices
Ne fragmentez pas votre application si l’un des points suivants est vrai :
- L’équipe est suffisamment petite pour que tout le monde puisse déployer l’ensemble de l’application en toute sécurité.
- Le domaine est encore en phase de découverte, les frontières seraient donc basées sur des suppositions.
- Le produit est à un stade précoce et les exigences changent chaque semaine.
- Vous ne disposez pas encore de la CI/CD, de l’observabilité et de la capacité d’astreinte nécessaires pour gérer de nombreux services.
Dans tous ces cas, un monolithe modulaire vous apporte la même discipline interne avec un déploiement unique, des transactions in-process et des refactorisations qui ne nécessitent pas de plan de migration. Comme ses modules communiquent via des interfaces explicites, un module peut être extrait pour devenir un service plus tard, lorsqu’une raison concrète apparaîtra : une équipe ayant besoin de sa propre cadence, un composant avec un profil de mise à l’échelle réellement différent, ou une contrainte de conformité. Extraire un module propre coûte bien moins cher que de fusionner un découpage mal conçu.
Migrer avec le pattern “strangler fig”
Lorsque vous fractionnez un système existant, faites-le de manière incrémentale. Le pattern strangler fig consiste à router le trafic via une façade et à déplacer chaque fonctionnalité une par une, jusqu’à ce que l’ancien code ne soit plus utilisé et puisse être supprimé.
- Trouver une couture. Choisissez une fonctionnalité qui est déjà assez autonome et qui évolue souvent. Une frontière de module nette est le candidat idéal.
- Mettre en place une façade. Routez le trafic concerné via un proxy ou une passerelle afin de pouvoir le basculer sans impacter les clients.
- Extraire le module dans un service. Donnez-lui son propre déploiement et sa propre pipeline, et déplacez ses tables dans une base de données dont il est propriétaire.
- Synchroniser les données avec prudence. Utilisez le double-write ou publiez des événements et effectuez un backfill jusqu’à ce que le nouveau stockage devienne la source de vérité, puis arrêtez d’écrire dans les anciennes tables.
- Basculer et supprimer. Transférez le trafic, surveillez les métriques et supprimez l’ancien chemin de code une fois qu’il n’est plus sollicité.
Ne commencez jamais par une réécriture complète (“big-bang”). L’avantage de l’approche strangler est que chaque étape est réversible et que le système reste en production tout au long du processus.
Définir le contrat en premier
Un contrat HTTP interne peut facilement dériver, car le producteur et le consommateur sont écrits par des équipes différentes et seuls les tests d’intégration permettent de s’en rendre compte. Définir le contrat sous forme de schéma avant d’écrire l’un ou l’autre des côtés permet de garantir la cohérence et vous permet de générer des clients, des serveurs et de la documentation à partir d’une source unique.
// inventory.proto
service Inventory {
rpc GetStock(GetStockRequest) returns (StockLevel);
rpc Reserve(ReserveRequest) returns (Reservation);
}
message GetStockRequest { string sku = 1; }
message StockLevel { string sku = 1; int32 available = 2; }
Pour HTTP, un document OpenAPI joue le même rôle. Dans les deux cas, le schéma est l’artefact soumis à revue, et un changement disruptif (breaking change) apparaît dans un diff plutôt qu’en production. Les tests de contrat pilotés par le consommateur (consumer-driven contract tests) bouclent la boucle en encodant ce que chaque consommateur utilise réellement, permettant ainsi au producteur de savoir, avant la mise en production, si sa modification est sans risque pour les appelants existants.
Adapter le transport à l’interaction
L’erreur classique consiste à utiliser un seul type de transport pour tout. Un système purement REST sérialise chaque réaction derrière une chaîne d’appels ; un système purement basé sur les événements rend une simple recherche absurdement indirecte. Choisissez le transport par interaction, et non par système.
| Interaction | Transport | Pourquoi |
|---|---|---|
| Navigateur vers backend | REST/JSON | Omniprésent, mettable en cache, facile à déboguer |
| Service à service, faible latence | gRPC | Typé, compact, clients générés |
| Réaction “fire-and-forget” | Events | Pas de couplage temporel, mis en tampon |
| Workflow de longue durée | Events plus une saga | Durable, répétable, compensé |
| Lecture à haut volume | Cache ou modèle de lecture | Évite complètement l’appel |
Une approche par défaut utile consiste à rendre les écritures faisant autorité via un appel synchrone lorsque l’appelant a besoin de la réponse, et à publier un event pour chaque effet pour lequel l’appelant n’a pas besoin d’attendre.
Les clés d’idempotence en pratique
Les Sagas, les tentatives de rejeu (retries) et les événements « at-least-once » signifient qu’une même opération peut arriver deux fois. La seule défense efficace face à la concurrence est un contrôle de déduplication qui soit atomique avec l’effet de bord.
export async function capturePayment(cmd: CapturePayment) {
const key = `payment:captured:${cmd.orderId}`;
const inserted = await redis.set(key, "1", "NX", "EX", 86_400);
if (inserted === null) return { skipped: true };
return gateway.charge(
{ orderId: cmd.orderId, amountCents: cmd.amountCents },
{ idempotencyKey: cmd.orderId },
);
}
Trois détails sont essentiels. La clé est dérivée de l’opération métier, et non d’un identifiant de requête aléatoire, afin qu’un producteur qui enfile deux fois le même travail logique provoque toujours une collision. Le contrôle et l’écriture du marqueur constituent une seule opération atomique, empêchant ainsi deux workers concurrents de réussir simultanément. Enfin, le fournisseur reçoit sa propre clé d’idempotence, car votre marqueur peut être perdu alors que l’enregistrement du fournisseur subsiste.
Outbox, inbox et effets “exactly-once”
Un service qui écrit dans sa base de données puis publie un événement présente une faille : le processus peut planter entre les deux étapes, laissant l’état modifié mais l’événement non envoyé. Le pattern outbox comble cette lacune en écrivant l’événement dans une table outbox au sein de la même transaction locale que le changement d’état, puis en laissant un relais publier les lignes.
await db.transaction(async (tx) => {
await orders.save(order, tx);
await tx.insert(outbox).values({
id: randomUUID(),
topic: "orders.created",
key: order.id,
payload: order.toEvent(),
});
});
// A separate relay polls outbox and publishes, at least once.
Côté consommateur, c’est l’image miroir. Comme la livraison est de type “at-least-once”, un consommateur peut recevoir le même événement deux fois. Maintenez une table inbox des identifiants d’événements traités et ignorez les doublons, là encore dans la même transaction que l’effet produit. L’utilisation combinée de l’outbox et de l’inbox permet un traitement “effectively-once” sans prétendre que le réseau est fiable.
Bulkheads et dégradation gracieuse
Un circuit breaker arrête d’appeler une dépendance défaillante. Un bulkhead va plus loin en isolant les ressources pour qu’une seule dépendance ne puisse pas tout consommer. Si le service de recommandations possède son propre pool de connexions, un blocage à ce niveau ne pourra pas épuiser le pool dont dépend le processus de paiement.
const pools = {
checkout: new Pool({ max: 20 }),
recommendations: new Pool({ max: 5, timeoutMs: 500 }),
};
La dégradation est la partie visible pour l’utilisateur de ce même concept. Lorsqu’une dépendance non essentielle est lente, renvoyez une réponse réduite plutôt qu’une erreur. Une page produit sans recommandations personnalisées reste une bonne page ; une page produit qui expire parce que les recommandations ont expiré est une panne. Décidez pour chaque appel s’il est requis ou s’il s’agit d’un “best-effort”, et encodez cette décision là où l’appel est effectué.
La couche anti-corruption (Anti-Corruption Layer)
Lorsqu’un service consomme directement le modèle d’un autre, il en hérite les hypothèses, et tout changement de structure du Order du producteur se répercute sur chaque consommateur. Une couche anti-corruption est une fine couche de traduction qui mappe le contrat externe vers le langage métier propre au consommateur.
// Shipping has its own idea of a shipment. It translates the event.
function toShipment(event: OrderCreated): ShipmentRequest {
return {
reference: event.id,
destination: event.shippingAddress,
lines: event.lines.map((l) => ({ sku: l.sku, quantity: l.qty })),
};
}
Cette couche demande un peu de code supplémentaire, mais elle offre une véritable indépendance. Le consommateur peut renommer ses propres concepts librement, tolérer des champs inconnus et absorber un changement majeur provenant d’un tiers qu’il ne contrôle pas.
Savoir si le découpage fonctionne
Les microservices sont un moyen, pas une fin en soi ; mesurez donc ce que vous souhaitiez réellement obtenir. Les signaux indiquant que le découpage est rentable sont d’ordre organisationnel : le nombre d’équipes capables de déployer sans coordination, le délai (lead time) entre le commit et la production pour un service unique, et la fréquence à laquelle un seul changement impacte plusieurs services. Si ces chiffres ne s’améliorent pas, la topologie ne justifie pas son coût.
Les signaux indiquant que la situation se dégrade sont techniques et faciles à identifier :
- Les déploiements s’effectuent toujours dans un ordre fixe entre les services.
- Une seule action utilisateur génère une longue chaîne d’appels synchrones.
- Un changement de schéma dans un service nécessite la mise en production d’une autre équipe.
- L’astreinte est incapable de déterminer quel service est à l’origine d’un incident.
L’un de ces points signifie que vous avez un monolithe distribué, et la solution consiste généralement à fusionner une frontière plutôt qu’à ajouter un nouveau service.
Une checklist avant de scinder
Avant d’extraire un module d’un monolithe, assurez-vous que tous les points suivants sont respectés.
- Le module possède déjà ses propres données et personne d’autre n’écrit dans ses tables.
- Les autres modules dépendent de son interface, et non de son fonctionnement interne.
- Vous avez une raison qui dépasse le simple sentiment que « c’est devenu trop gros » : un rythme d’équipe différent, un profil de montée en charge spécifique ou une contrainte de conformité.
- Vous disposez de la CI/CD, du tracing et d’une capacité d’astreinte pour gérer un déploiement supplémentaire.
- Vous avez un plan pour la migration des données ainsi qu’une méthode pour effectuer un rollback.
- Vous avez décidé de la manière dont le module communiquera avec le reste du système, de façon synchrone ou asynchrone, et vous avez rédigé le contrat.
Si l’une de ces réponses est négative, l’extraction sera plus coûteuse qu’il n’y paraît. Corriger la frontière à l’intérieur du monolithe est presque toujours moins onéreux que de devoir le faire à travers un réseau.
Bonnes pratiques
- Divisez vos services pour favoriser le déploiement indépendant et l’autonomie des équipes, et non pour des raisons de montée en charge.
- Alignez chaque service sur un bounded context et attribuez-lui un responsable clair.
- Donnez à chaque service sa propre base de données et interdisez les requêtes entre services.
- Configurez des timeouts, des tentatives de reconnexion limitées avec jitter et des circuit breakers sur chaque appel.
- Rendez les opérations d’écriture idempotentes avant d’autoriser toute tentative de reconnexion.
- Privilégiez les événements pour les réactions qui ne nécessitent pas de réponse immédiate.
- Utilisez des sagas avec compensations plutôt que des transactions distribuées.
- Versionnez vos contrats et assurez-vous que chaque modification soit additive.
- Propagez un correlation id et instrumentez le tracing dès le premier service.
- Gardez la gateway légère et déplacez la logique métier vers les services.
- Extrayez vos services via le pattern strangler fig, une fonctionnalité à la fois.
Erreurs courantes
- Découper par couche technique, pour ensuite avoir besoin de trois services pour chaque fonctionnalité.
- Partager une base de données et s’étonner que les mises en production nécessitent encore une coordination.
- Déployer les services ensemble et appeler le résultat « microservices ».
- Ajouter des tentatives de reconnexion (retries) sans idempotence et facturer les clients deux fois.
- Ne pas configurer de timeouts, laissant ainsi une dépendance lente bloquer toute la chaîne d’appels.
- Utiliser des chaînes synchrones là où un événement permettrait de supprimer le couplage.
- Construire une saga sans chemin de compensation pour les cas d’échec.
- Négliger le traçage distribué et déboguer en devinant à travers les logs.
- Découper trop tôt, figeant ainsi des frontières erronées dans une infrastructure coûteuse.
- Considérer Kubernetes et un service mesh comme l’objectif plutôt que comme le coût pour atteindre cet objectif.
Et après ?
Si les compromis évoqués ici semblent trop lourds pour votre équipe, consultez le guide sur le Monolithe Modulaire — c’est le choix par défaut recommandé et la meilleure préparation pour un futur découpage. Pour permettre aux services de communiquer sans s’appeler directement, le guide sur l’Architecture Orientée Événements est l’étape suivante, avec Kafka pour gérer le journal d’événements. Enfin, une fois que vous gérez de nombreux services, le guide sur le Déploiement Cloud traite de la plateforme et de l’observabilité nécessaires pour maintenir leur stabilité.