Aller au contenu

Microservices

Des services autonomes, déployés independamment — une topologie puissante qui exige des prérequis solides.


Le principe

Les microservices découpent le système en services indépendants. Chaque service possédé son propre code, sa propre base de données, son propre cycle de release. Les services communiquent par le réseau — appels HTTP/gRPC synchrones ou événements asynchrones.

Cette topologie existe pour résoudre un problème précis : permettre a des équipes autonomes de livrer independamment, a des rythmes différents, sur des composants qui scalent differemment. Si on n'a pas ce problème, on n'a pas besoin de cette solution.

graph LR
    Client["Client"] --> GW["API Gateway"]
    GW --> OS["Order Service"]
    GW --> PS["Payment Service"]
    GW --> NS["Notification Service"]
    OS --> ODB[("Orders DB")]
    PS --> PDB[("Payments DB")]
    NS --> NDB[("Notif DB")]
    OS -->|"event"| PS
    PS -->|"event"| NS

Le diagramme montre la structure cible. Chaque service possédé sa base de données — c'est le pattern "database per service". Les communications inter-services passent par des événements asynchrones pour les side effects et par des appels synchrones uniquement quand une réponse immédiate est nécessaire.


Les 5 prérequis

Avant d'envisager les microservices, cinq prérequis doivent être reunis. L'absence d'un seul suffit à transformer le projet en monolithe distribué.

  1. CI/CD mature — chaque service doit pouvoir être déployé independamment, sans coordination manuelle. Sans ca, un deploy implique de synchroniser plusieurs équipes et on perd le benefice principal.

  2. Monitoring distribué — traces, logs centralises, alerting par service. Debugger un appel qui traverse 4 services sans tracing est quasi impossible. OpenTelemetry, Jaeger, Grafana ne sont pas des options — ce sont des necessites.

  3. Équipe suffisante — la 2-pizza rule s'applique par service : si une équipe ne peut pas owneriser un service seul, la frontiere est mauvaise. Un service sans owner devient un service zombie qui accumule la dette technique.

  4. Domaine bien découpé — les bounded contexts sont identifiés et stables. Sinon les services se retrouvent couples par des appels synchrones en chaîne, formant un monolithe distribué.

  5. Besoin réel de scalabilité indépendante — un service reçoit 100x plus de trafic qu'un autre et doit scaler séparément. Si tout le système scale ensemble, les microservices n'apportent pas ce benefice.

Prérequis manquants = ralentissement garanti

Si vous n'avez pas les 5 prérequis, les microservices vous ralentiront. Le monolithe modulaire est un meilleur point de départ. On peut toujours extraire un module plus tard — on ne peut pas facilement recoller des services mal decoupes.


Stratégies de decomposition

La question centrale : comment découper ? Deux approches dominent, et elles ne s'excluent pas.

Par capacité métier (business capability)

On identifie les capacités métier de l'entreprise — ce qu'elle fait, pas comment elle est organisée. Chaque capacité devient un service candidat.

Capacites metier d'un e-commerce :
  - Gestion du catalogue produit
  - Gestion des commandes
  - Traitement des paiements
  - Gestion des expeditions
  - Support client
  - Programme de fidelite

L'avantage : les capacités métier changent rarement. Le catalogue existait avant le web, il existera après. Le découpage est stable dans le temps.

Par sous-domaine (subdomain)

On utilise le DDD strategique pour identifier les sous-domaines : core (avantage concurrentiel), supporting (nécessaire mais pas differenciateur), generic (commodity).

Type de sous-domaine Stratégie Exemple
Core Investir, développer en interne, équipe dédiée Moteur de recommandation, pricing dynamique
Supporting Développer en interne mais sans sur-investir Gestion des expeditions, notifications
Generic Acheter ou intégrer un SaaS Authentification, envoi d'emails, paiement

Les sous-domaines core meritent des services dédiés avec des équipes dédiées. Les sous-domaines generic n'ont souvent pas besoin d'être des services — un appel a un SaaS suffit.


Database per service

Chaque service possédé sa propre base de données. Aucun autre service n'accede directement a cette base. C'est la règle la plus importante et la plus violee des microservices.

graph TB
    subgraph "Correct"
        S1["Order Service"] --> D1[("Orders DB")]
        S2["Payment Service"] --> D2[("Payments DB")]
        S1 -->|"API call"| S2
    end

Pourquoi cette règle est non-negociable :

  • Sans elle, un changement de schéma dans une base affecte plusieurs services — on a un monolithe distribué
  • Les services ne peuvent pas évoluer leur modèle de données independamment
  • Les performances d'un service impactent les autres via des requêtes sur la même base

Conséquences sur la cohérence des données :

La séparation des bases de données implique qu'on ne peut plus faire de transactions ACID cross-services. Il faut accepter la cohérence eventuelle (eventual consistency) et utiliser des patterns spécifiques :

Pattern Quand l'utiliser Complexité
Saga (choreographie) 2-3 services, flux lineaire Moyenne
Saga (orchestration) 4+ services, flux complexe avec branchements Élevée
Outbox pattern Garantir publication d'événement + écriture locale atomique Moyenne
CQRS + projections Construire des vues de lecture cross-services Élevée

La saga est le pattern le plus courant. Dans une saga chorographiee, chaque service publie un événement qui déclenche le suivant. Dans une saga orchestree, un coordinateur central pilote la sequence.

sequenceDiagram
    participant O as Order Service
    participant P as Payment Service
    participant I as Inventory Service
    O->>P: PaymentRequested
    P->>P: Traiter le paiement
    P->>O: PaymentConfirmed
    P->>I: ReserveStock
    I->>I: Reserver le stock
    I->>O: StockReserved
    O->>O: Confirmer la commande

Coût opérationnel

Le coût opérationnel des microservices est significatif et souvent sous-estime. Il ne s'agit pas d'un coût ponctuel mais d'un coût permanent que chaque équipe paie à chaque release.

Composant Ce que ca implique
Service mesh Istio, Linkerd — gestion du trafic, mTLS, retries
Tracing distribué OpenTelemetry, Jaeger — suivre une requête sur 5 services
Contract testing Pact, Dredd — garantir que les APIs entre services restent compatibles
Deployment orchestration Kubernetes, Nomad — scheduler des dizaines de services
Health checks et circuit breakers Resilience4j — éviter la propagation de pannes en cascade

Observabilité

L'observabilité n'est pas du monitoring amélioré — c'est la capacité a comprendre l'état interne d'un système distribué à partir de ses outputs. Les trois piliers :

  • Logs structures — JSON avec correlation ID, pas du texte libre. Chaque requête qui traverse N services porte le même correlation ID.
  • Métriques — latence par service (p50, p95, p99), taux d'erreur, saturation des files d'attente.
  • Traces — arbre complet d'un appel cross-services, avec la durée de chaque étape.

Debugging

Debugger en microservices est fondamentalement différent. Le problème n'est plus "quelle ligne de code a plante" mais "quel service, dans quel état, avec quelles données en entree, à quel moment". Les outils de debugging local (breakpoints, step-through) sont largement inutiles quand le bug est une interaction entre 3 services.

Les équipes matures investissent dans le replay : la capacité a rejouer une requête problematique dans un environnement de staging avec les mêmes données. Ca demande une infrastructure de logging et de capture des messages qui n'est pas triviale.


L'anti-pattern du monolithe distribué

Le monolithe distribué est le pire des deux mondes : des services déployés séparément, mais tellement couples qu'ils doivent être déployés ensemble. On à la complexité des microservices sans aucun de leurs benefices.

Signes d'un monolithe distribué en formation :

  • Un service ne peut pas démarrer sans qu'un autre soit déjà disponible
  • Un changement de schéma de base de données affecte 3 services
  • Les releases exigent une coordination entre équipes
  • Les tests d'intégration necessitent de lancer 5 services localement
  • Un service ne peut pas être teste sans mocker 4 autres services

La cause la plus fréquente : des bounded contexts mal identifiés au départ, menant a des services qui partagent des concepts fondamentaux et s'appellent mutuellement de façon synchrone pour chaque opération.

Le test du deploy indépendant

Chaque service doit pouvoir être déployé independamment, a n'importe quel moment, sans coordination avec les autres équipes. Si ce n'est pas le cas, on a un monolithe distribué — pas des microservices.


Quand choisir les microservices

Signal Pourquoi ca justifie les microservices
Équipes > 15 personnes Plusieurs équipes autonomes ont besoin de déployer independamment
Scalabilité differentielle Un composant reçoit 50x plus de trafic et doit scaler seul
Polyglotte technologique Certains composants beneficient de runtimes différents (ML en Python, API en Go)
Fréquence de deploy différente Un composant change 10x plus souvent que les autres
Isolation de panne critique Un bug dans un composant ne doit pas affecter les autres

La question a se poser n'est pas "est-ce que les microservices sont mieux ?" mais "est-ce que le coût opérationnel permanent est justifie par un benefice réel que le monolithe modulaire ne peut pas offrir ?". Pour la majorité des projets, la réponse est non.


Chapitre suivant : Event-driven — communication par événements, découplage temporel et cohérence eventuelle.