Database per Service : quand ça vaut vraiment la complexité
Database per Service : quand ça vaut vraiment la complexité
J'ai accompagné un client dans le secteur du commerce en ligne qui avait adopté Database per Service dès le début, par principe, avant même d'avoir 5 développeurs. 18 mois plus tard, l'équipe de 6 développeurs backend passait 30% de son temps à gérer l'infrastructure de 20 bases de données, les sagas qui échouaient partiellement, et les incohérences de données entre services.
La décision que nous avons prise ensemble : migrer 15 des 20 services vers un schéma-per-service dans une base partagée. Seuls les 5 services critiques (ceux qui avaient vraiment des besoins différents de performance, d'isolation, ou de moteur) ont conservé leur base dédiée.
Résultat : le temps passé à l'infrastructure est passé de 30% à 8%. L'équipe a pu réinvestir ces heures dans de la valeur produit.
Database per Service n'est pas un impératif des microservices. C'est un trade-off. Et comme tout trade-off, il se décide selon le contexte, pas selon un dogme.
Le problème réel que Database per Service résout
Sam Newman dans "Building Microservices" (2014) a popularisé l'idée que chaque microservice devrait posséder ses données. L'intention était valide : éviter le couplage de base de données partagée qui crée des dépendances implicites entre services.
Ces couplages sont réels :
Couplage de schéma : si le service A modifie la table orders, le service B qui lit cette table peut casser sans que l'équipe A s'en rende compte.
Couplage de déploiement : une migration de base de données doit être coordonnée avec tous les services qui lisent ou écrivent les tables concernées.
Couplage de performance : une requête lente du service A consomme des ressources de connexion qui dégradent les performances du service B.
Couplage de technologie : tous les services sont contraints d'utiliser le même moteur de base de données, même si certains auraient besoin d'un graph database et d'autres d'un document store.
Ces couplages valent la peine d'être éliminés, mais la question est : à quel coût, et avec quelle alternative ? Et la décision de les éliminer (ou de les tolérer) mérite d'être consignée dans un Architecture Decision Record pour que le contexte reste accessible à toute l'équipe.
Le coût réel de Database per Service
Complexité opérationnelle : 10 services = 10 bases de données à provisionner, monitorer, sauvegarder, mettre à jour, et maintenir en haute disponibilité. Sur AWS, 10 instances RDS PostgreSQL coûtent entre 500 et 2 000€ par mois selon la taille, avant les coûts d'ingénierie pour les gérer.
Transactions distribuées : sans base de données partagée, une opération qui modifie des données dans plusieurs services (créer une commande ET décrémenter le stock ET enregistrer le paiement) ne peut plus être traitée dans une transaction ACID.
Il faut implémenter le pattern Saga, décrit en détail par Vernon Vaughn dans "Implementing Domain-Driven Design" :
Saga chorégraphie pour la création d'une commande :
OrderService.createOrder() → publie OrderCreated
→ InventoryService réserve le stock → publie InventoryReserved
→ PaymentService charge le paiement → publie PaymentProcessed
→ OrderService confirme la commande
En cas d'échec du paiement :
→ PaymentService publie PaymentFailed
→ InventoryService libère la réservation
→ OrderService annule la commande
Chaque étape peut échouer, les messages peuvent être perdus ou dupliqués, et les états intermédiaires sont difficiles à debugger. Pour renforcer la robustesse de ces flux, les patterns de résilience : circuit breaker, retry, timeout sont indispensables dès lors que les services communiquent de manière asynchrone.
Requêtes cross-services : une requête qui joint des données de plusieurs services (liste des commandes avec le nom du client et le stock disponible) devient une API composition : plusieurs appels HTTP en séquence ou en parallèle, avec de la logique d'agrégation en mémoire. Cette communication asynchrone entre services introduit un couplage temporel qu'il faut gérer explicitement, notamment avec des garanties d'idempotence et d'ordre sur les événements.
Vous migrez vers les microservices et vous hésitez sur la stratégie de base de données ?
Choisir la bonne stratégie de base de données pour une architecture distribuée dépend de nombreux facteurs contextuels que j'ai appris à évaluer sur le terrain, dans la finance, les médias, la logistique. En 30 minutes, on peut évaluer les trade-offs et définir l'approche adaptée à votre situation réelle.
Le cadre de décision objectif
La question n'est pas "devrions-nous faire Database per Service ?", c'est "quel niveau de couplage de base de données est acceptable dans notre contexte ?"
| Critère | Favorise Database per Service | Favorise Base partagée |
|---|---|---|
| Taille de l'équipe | > 15 développeurs, équipes indépendantes | < 15 développeurs, une seule équipe |
| Fréquence de changement de schéma | Fréquente (plusieurs fois par semaine) | Rare (quelques fois par mois) |
| Exigences de performance | Services avec des profils d'usage très différents | Profils d'usage similaires |
| Exigences de disponibilité | SLAs différents par service | SLA uniforme |
| Transactions cross-services | Rares | Fréquentes et critiques |
| Maturité opérationnelle | SRE dédié, infrastructure mature | Pas d'équipe infra dédiée |
La règle empirique : si les équipes qui développent les services peuvent déployer indépendamment et que les transactions cross-services sont rares, Database per Service a du sens. Sinon, le coût dépasse la valeur.
Les alternatives ignorées
Avant de sauter à "une base de données par service", il existe des options intermédiaires qui réduisent le couplage sans la complexité complète.
Option 1 : Schema per Service (dans la même base de données)
Chaque service possède son schéma (namespace) dans une base de données partagée. Le service A ne peut accéder qu'aux tables du schéma A.
-- Service A n'accède qu'à son schéma
SET search_path TO ordering;
SELECT * FROM orders; -- → ordering.orders
-- Service B n'accède qu'à son schéma
SET search_path TO inventory;
SELECT * FROM products; -- → inventory.products
Avantages : les transactions ACID restent possibles si nécessaire. Complexité opérationnelle minimale (une seule base de données).
Option 2 : Read replicas dédiées par service
La base de données principale est partagée pour les écritures, mais chaque service lit depuis sa propre replica read-only synchronisée. Utile quand le problème principal est la contention de lecture.
Option 3 : CQRS sans séparation de base
Séparer les modèles de lecture et d'écriture sans nécessairement avoir des bases de données séparées. Les requêtes complexes lisent des vues matérialisées maintenues à jour par des événements.
Quand migrer vers Database per Service
Les signaux que la base partagée devient un problème :
- Les migrations de base de données nécessitent de coordonner 3 équipes ou plus
- Un service lent dégrade régulièrement les performances des autres
- Deux services ont des besoins contradictoires sur le schéma d'une table partagée
- L'équipe veut utiliser un moteur de base de données différent pour un service spécifique
La migration progressive : commencer par les tables du service le plus indépendant. Migrer la table, mettre en place la synchronisation des données si nécessaire (CDC avec Debezium, événements de domaine), valider, puis passer à la table suivante.
Plan de migration en 4 phases :
Phase 1 : service de notifications (aucune dépendance cross-service)
Phase 2 : service de recherche (lecture seule, pas d'écriture cross-service)
Phase 3 : service de catalogue produit (écriture rarement liée aux autres)
Phase 4 : service de commandes (transactions complexes — laisser pour dernier)
Le cas particulier du polyglot persistence
Database per Service devient moins coûteux quand les services utilisent des bases de données managées (DynamoDB, MongoDB Atlas, Redis Cloud) avec une facturation à l'usage. Le coût opérationnel est absorbé par le fournisseur.
Le pattern polyglot persistence (chaque service utilise le moteur adapté à ses besoins) ne vaut sa complexité que si les besoins sont vraiment distincts et que l'équipe a la maturité opérationnelle pour gérer plusieurs moteurs.
- Service de recherche → Elasticsearch
- Service de sessions → Redis
- Service de catalogue → MongoDB (documents flexibles)
- Service de commandes → PostgreSQL (transactions ACID)
FAQ sur Database per Service
1. Comment gérer la cohérence des données entre services sans transactions distribuées ?
Eventual consistency avec compensation. Accepter que les données ne soient pas instantanément cohérentes entre services, seulement éventuellement cohérentes. Pour les cas où une cohérence forte est nécessaire, utiliser le pattern Outbox : écrire l'événement dans la même transaction que la donnée, puis publier l'événement de façon asynchrone. Pour les cas d'échec, implémenter des compensating transactions explicites plutôt que des rollbacks automatiques.
2. Peut-on utiliser des transactions distribuées (2PC) à la place des Sagas ?
Techniquement oui, mais déconseillé. Le Two-Phase Commit est lent (lock pendant la phase de préparation), fragile (coordinator failure = système bloqué), et complexe à implémenter correctement. Les Sagas sont plus complexes à concevoir mais plus robustes en production. La recommandation de Sam Newman et de l'industrie : éviter 2PC, préférer les Sagas ou revoir l'architecture pour réduire les transactions cross-services.
3. Comment gérer les reportings qui nécessitent des données de plusieurs services ?
Data warehouse ou OLAP dédié. Chaque service publie ses événements vers un pipeline de données (Kafka → Spark/Flink → Data Warehouse). Les analyses et rapports lisent le data warehouse, pas les bases des services. C'est le principe CQRS à l'échelle de l'architecture : les services gèrent les écritures, le data warehouse gère les lectures analytiques complexes.
4. Quel outil utiliser pour la synchronisation de données entre services ?
Change Data Capture (CDC) avec Debezium : il capture les changements PostgreSQL/MySQL en temps réel et les publie sur Kafka. Pour des cas plus simples : événements de domaine publiés sur une queue à chaque changement d'état. L'approche CDC est plus robuste (aucun changement applicatif requis) mais plus complexe à opérer. Les événements de domaine sont plus simples mais nécessitent une discipline applicative.
5. Database per Service est-il compatible avec les architectures serverless (Lambda, Cloud Functions) ?
Oui, et c'est souvent plus simple. Les bases de données managées à l'usage (DynamoDB, Firestore, Aurora Serverless) s'adaptent naturellement au modèle serverless : pas de pool de connexions à gérer, facturation à la requête. Le principal défi est la gestion des connexions (Lambda peut créer des milliers de connexions simultanées), résolu par des proxy de connexions comme RDS Proxy pour PostgreSQL/MySQL.
Ressource gratuite : Engineering Maturity Self-Assessment
L'Engineering Maturity Self-Assessment couvre le domaine Architecture Distribuée : évaluez votre maturité sur le découpage des services, la gestion des données, et la résilience. Score et plan d'action en 10 minutes.