Architecture microservices : les principes qui comptent vraiment
Les microservices, ce ne sont pas juste des "petits services". C'est un pari architectural et organisationnel avec de vrais compromis. Voici les principes qui séparent une architecture microservices saine d'un monolithe distribué.
Migrer vers les microservices est une conversation. Concevoir une architecture microservices qui ne s'effondre pas en monolithe distribué en est une autre. Voici les principes qui séparent les deux, que vous migriez un système existant ou que vous construisiez en partant de zéro.
D'abord : en avez-vous besoin ?
Les microservices échangent de la simplicité contre du scaling indépendant et de l'autonomie d'équipe. Ce compromis vaut le coup une fois qu'une partie du système a réellement besoin de scaler ou de se déployer seule, ou quand plusieurs équipes se bloquent dans une seule codebase. Jusque-là, un monolithe modulaire, des frontières de modules claires et des dépendances imposées dans un seul déployable, vous donne l'essentiel de la structure sans le réseau, et c'est le bon défaut.
Les frontières suivent les capacités métier, pas les couches techniques
L'erreur la plus courante est de découper les services par couche technique : un service models, un service controllers, un service auth qui ne possède rien. Les frontières devraient suivre les capacités métier et les bounded contexts. Un service Orders, un service Shipping, un service Billing, chacun possédant une tranche verticale complète de son domaine, de ses données à son API. Si deux services changent toujours ensemble, ils n'en font qu'un.
Chaque service possède ses données, sans exception
Une base partagée est le moyen le plus rapide d'arriver à un monolithe distribué : tout changement de schéma nécessite désormais des déploiements coordonnés entre équipes, et les services indépendants ne le sont que de nom. Un service accède aux données d'un autre service uniquement via son API ou ses événements, jamais ses tables.
// Anti-pattern : Shipping lit directement la base Orders
$order = DB::connection('orders_db')->table('orders')->find($orderId);
// Pattern : Shipping interroge le service Orders via son API
$order = Http::get(config('services.orders.url') . "/orders/{$orderId}")->json();Communication : choisir sync ou async délibérément
- ✓REST ou gRPC synchrone uniquement quand l'appelant a besoin d'une réponse immédiate : authentification, autorisation de paiement.
- ✓Événements ou queues asynchrones pour tout ce qui peut arriver plus tard : notifications, analytics, logs d'audit, mises à jour inter-services.
- ✓Éviter les chaînes synchrones bavardes où A attend B qui attend C ; latence et échec s'accumulent à chaque saut.
Transactions distribuées : sagas et outbox
Vous ne pouvez pas exécuter une transaction de base de données entre services. Une opération métier qui traverse plusieurs services devient une saga : une séquence de transactions locales, chacune publiant un événement qui déclenche la suivante, avec une action de compensation si une étape échoue. Pour publier cet événement de façon fiable dans la même transaction que l'écriture locale, utilisez le pattern outbox : écrivez l'événement dans une table outbox dans le même commit, puis un processus séparé l'envoie au broker.
Concevoir pour l'échec, rien n'est optionnel
- ✓Timeouts et circuit breakers sur chaque appel réseau, pour qu'une dépendance lente ne provoque pas une panne totale en cascade.
- ✓Retries avec backoff exponentiel, associés à des clés d'idempotence pour qu'une écriture retentée ne duplique pas les données.
- ✓Bulkheads : isoler les ressources par dépendance pour qu'un service en aval en difficulté ne puisse pas épuiser tous les threads ou toutes les connexions.
- ✓Dégradation gracieuse : décider ce que fait le service quand une dépendance est en panne, avant de le découvrir en production.
L'observabilité est un prérequis, pas un ajout
Une seule requête utilisateur peut toucher six services. Sans tracing distribué (OpenTelemetry), sans ID de corrélation propagé à chaque appel, et sans logs structurés centralisés, déboguer cette requête est de la devinette déguisée en rapport d'incident. Suivez les métriques RED par service, débit, erreurs, durée, et alertez dessus.
Le déploiement indépendant est le test
La mesure la plus claire pour savoir si vous avez vraiment des microservices : pouvez-vous déployer un service, à tout moment, sans coordination avec une autre équipe ni redéploiement de quoi que ce soit d'autre. Sinon, vous avez un monolithe distribué, et les appels réseau entre vos services sont du coût pur.
| Signal | Microservices sains | Monolithe distribué |
|---|---|---|
| Déploiement | N'importe quel service, à tout moment, indépendamment | Les services doivent déployer ensemble |
| Données | Chaque service possède sa base | Base partagée ou lectures de tables inter-services |
| Une panne en aval | Dégrade une fonctionnalité | Se propage à des fonctionnalités indépendantes |
| Ajouter un champ | Un service, un déploiement | Changement coordonné entre équipes |
| Communication | Surtout des événements async | Chaînes d'appels synchrones profondes |
Le signe le plus clair d'un monolithe distribué : impossible de déployer un service sans redéployer les autres, ou un bug dans l'un fait tomber des fonctionnalités qui devraient être indépendantes. Si c'est votre réalité, vous avez des microservices de nom et un monolithe en pratique, avec de la latence réseau en plus.
FAQ
- Quelle taille pour un microservice ?
- Dimensionnez-le à un bounded context, une capacité métier possédée de bout en bout, pas à un nombre de lignes. Un bon test : il peut être compris, modifié et déployé par une équipe sans coordination avec les autres. Si deux services changent toujours ensemble, ils n'en font qu'un.
- Faut-il une base de données par microservice ?
- Oui. Une base partagée couple les services au niveau du schéma, donc tout changement demande des déploiements coordonnés et l'indépendance n'est que nominale. Les services lisent les données des autres via des API ou des événements, jamais via les tables de l'autre service.
- Comment gérer une transaction entre microservices ?
- Vous ne pouvez pas utiliser une transaction de base de données entre services. Modélisez l'opération en saga : une chaîne de transactions locales, chacune émettant un événement qui déclenche la suivante, avec des actions de compensation pour annuler les étapes précédentes si l'une échoue. Publiez ces événements de façon fiable avec le pattern outbox.
- Qu'est-ce qu'un monolithe distribué ?
- Un système découpé en services séparés qui ne peuvent toujours pas être déployés ou modifiés indépendamment, en général parce qu'ils partagent une base ou dépendent de chaînes d'appels synchrones profondes. Il a le coût opérationnel des microservices et le couplage d'un monolithe, plus la latence réseau.
- Microservices ou monolithe modulaire ?
- Commencez par un monolithe modulaire : des frontières de modules imposées dans un seul déployable. Il vous donne l'essentiel de la discipline architecturale sans le coût des systèmes distribués. Sortez un module en service séparé seulement quand il a réellement besoin de scaler ou de se déployer indépendamment.
Les microservices sont un outil pour un problème précis : des parties d'un système qui doivent scaler ou évoluer indépendamment, et des équipes qui ne doivent pas se bloquer. Appliqués là, avec la propriété des données, la communication async, la gestion des échecs et une vraie observabilité, ils paient. Appliqués par défaut, c'est un impôt.
Besoin d'aide sur ce sujet ? Migration Microservices
Découvrir ce service →