Microservices - 03 - Communication inter-services : REST, gRPC et messages

Les trois facons de faire communiquer des microservices : REST, gRPC et messaging asynchrone. Quand utiliser chaque approche.

03 - Communication inter-services : REST, gRPC et messages

Ce que tu vas apprendre

  • Les trois modes de communication entre microservices
  • Quand choisir REST, gRPC ou le messaging asynchrone
  • Les pièges de la communication synchrone

Prerequisites

02 - Decouper un monolithe


Le problème fondamental

Dans un monolithe, deux modules communiquent par appel de fonction. C'est rapide, fiable, et type. En microservices, cette communication passe par le réseau. Et le réseau, ca echoue. Ca timeout. Ca ralentit. Ca perd des paquets.

Choisir le bon mode de communication, c'est une des décisions les plus importantes d'une architecture microservices. Il y a trois grandes familles.

Synchrone : REST (HTTP/JSON)

REST, c'est le choix par défaut. Le service A envoie une requête HTTP au service B et attend la réponse.

typescript// Service Commandes appelle le service Catalogue
const product = await fetch("http://catalogue-service:3000/products/abc-123")
  .then(r => r.json());

Quand utiliser REST :

  • API publiques (navigateurs, apps mobiles, partenaires)
  • Communication simple requête/réponse
  • Quand la lisibilité et le debug comptent plus que la performance

Limites :

  • JSON est verbeux (chaque champ est répété en texte)
  • Pas de typage fort cote réseau (tu peux envoyer n'importe quoi)
  • HTTP/1.1 : une connexion par requête
  • Pas de streaming natif (il faut du SSE ou du WebSocket a cote)

REST reste le meilleur choix pour les APIs publiques. Pour le trafic interne entre services, il y a mieux.

Synchrone : gRPC (HTTP/2 + Protobuf)

gRPC est un framework RPC créé par Google. Il utilise HTTP/2 pour le transport et Protocol Buffers pour la sérialisation. C'est plus rapide, plus compact, et fortement type.

protobuf// catalogue.proto
service CatalogueService {
  rpc GetProduct (GetProductRequest) returns (Product);
}

message GetProductRequest {
  string product_id = 1;
}

message Product {
  string id = 1;
  string name = 2;
  int32 price_cents = 3;
}

Quand utiliser gRPC :

  • Communication interne entre services (là où la performance compte)
  • Quand tu as besoin de contrats types stricts
  • Streaming de donnees (logs en temps réel, flux de metriques)
  • Equipes polyglotes (le .proto généré du code dans tous les langages)

Limites :

  • Binaire = pas lisible dans un curl (il faut grpcurl)
  • Pas supporte nativement par les navigateurs (il faut gRPC-Web)
  • Courbe d'apprentissage plus raide que REST

La serie gRPC : communication inter-services couvre le sujet en profondeur -- du fichier .proto au déploiement en production.

Asynchrone : les messages

Le service A envoie un message dans une queue. Le service B le consomme quand il peut. Les deux ne sont jamais connectes directement.

typescript// Service Commandes publie un evenement
await messageQueue.publish("order.created", {
  orderId: "ord-456",
  userId: "usr-789",
  items: [{ productId: "abc-123", quantity: 2 }]
});

// Service Notifications consomme l'evenement (dans un autre processus)
messageQueue.subscribe("order.created", async (event) => {
  await sendEmail(event.userId, `Commande ${event.orderId} confirmee`);
});

Technologies courantes :

  • RabbitMQ : le classique, robuste, routing flexible
  • NATS : leger, rapide, bon pour le cloud-native
  • Redis Streams : si tu as deja Redis, c'est le plus simple
  • Apache Kafka : pour les gros volumes (mais complexe a opérer)

Quand utiliser le messaging :

  • Fire-and-forget : "la commande est créée, envoie un email"
  • Quand le producteur ne doit pas attendre le consommateur
  • Event-driven architecture : les services reagissent aux événements
  • Quand tu veux decoupler temporellement les services

Limites :

  • Eventual consistency (l'email partira, mais pas forcement dans la seconde)
  • Plus dur a debugger (les messages sont invisibles sans tooling)
  • Gestion des messages en échec (dead letter queues)
  • Ordre des messages pas toujours garanti

Le comparatif

Critère REST gRPC Messaging
Latence 5-20ms 1-5ms Variable (async)
Taille payload Gros (JSON texte) Compact (binaire) Variable
Typage Faible (OpenAPI optionnel) Fort (.proto obligatoire) Faible (schema optionnel)
Streaming Non natif 4 modes de streaming Non applicable
Couplage temporel Fort (A attend B) Fort (A attend B) Faible (A et B independants)
Debug Facile (curl, browser) Moyen (grpcurl, reflection) Dur (queue invisible)
Browser Natif gRPC-Web nécessaire WebSocket/SSE

La bonne combinaison

En pratique, une architecture microservices utilise les trois. Pas un seul.

REST pour les APIs publiques et les endpoints consommes par le frontend. Les navigateurs parlent HTTP/JSON, pas la peine de lutter.

gRPC pour la communication interne entre services. Le typage fort évité les bugs d'intégration, la performance est meilleure, et le streaming permet des use cases impossibles en REST. L'article sur les serveurs gRPC en TypeScript montre comment mettre ca en place.

Messaging pour tout ce qui n'a pas besoin de réponse immediate. Notifications, analytics, synchronisation de donnees entre services. Si le service B tombe, les messages s'accumulent dans la queue et seront traites quand il revient.

Sur paltemps.fr, tout est dans le meme processus, donc la question ne se pose pas. Un appel de fonction, c'est la communication inter-modules la plus rapide et la plus fiable qui existe. C'est un des avantages du monolithe qu'on oublie souvent.

Le piège de la cascade synchrone

Un anti-pattern classique : le service A appelle B qui appelle C qui appelle D. Chaque appel ajoute de la latence et un point de defaillance. Si D est lent, tout est lent. Si D tombe, tout tombe.

[Client] -> [API Gateway] -> [Service A] -> [Service B] -> [Service C]
                                                              |
                                                              v
                                                          [Service D]

Latence totale = latence A + latence B + latence C + latence D
Point de defaillance = A * B * C * D (si un tombe, tout tombe)

La solution : limiter la profondeur des appels synchrones (max 2-3 niveaux), utiliser du messaging pour les opérations qui n'ont pas besoin de réponse immediate, et mettre des circuit breakers pour éviter les cascades. J'en reparle dans l'article sur les erreurs classiques.


Résumé

  • REST pour les APIs publiques : simple, universel, debuggable
  • gRPC pour le trafic interne : rapide, type, streaming
  • Messaging pour le fire-and-forget : découplé, resilient
  • En pratique, utilise les trois selon le besoin
  • Attention aux cascades synchrones qui multiplient la latence et les points de defaillance

Article précédent : 02 - Decouper un monolithe Article suivant : 04 - L'infra : Docker, orchestration et service discovery

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.