Multi-datasource Spring Boot : 3 bases, 1 transaction
Comment on a séparé l'édition back-office, la lecture API mobile et l'analytique en trois bases PostgreSQL distinctes, tout en gardant des transactions atomiques entre elles. Retour d'expérience sur un back-end d'application d'accessibilité indoor (Java 17 / Spring Boot 3).
Pourquoi une seule base ne tenait plus
Le produit est une app mobile de navigation indoor pour l'accessibilité, déployée dans de grands ERP (aéroports, gares, musées). Elle s'appuie sur un back-office Spring Boot qui sert deux mondes différents :
côté édition, une dizaine d'opérateurs métier mettent à jour des plans complexes (places, levels, troubles, audit trail complet). Côté lecture, des milliers d'apps mobiles tirent les plans publiés depuis nos APIs.
Avec une seule base, les écritures intenses du BO entraient en contention avec les lectures massives des apps. Et les jointures lourdes nécessaires côté édition pénalisaient la latence mobile. Il fallait découpler.
3 bases, 1 application
On a séparé en trois datasources PostgreSQL côte à côte, gérés par la même application Spring Boot :
audit complet
read-only · versionnée
écritures massives
L’API mobile lit uniquement published_data — jamais working_data. Le job batch (Spring Batch + Quartz) copie + dénormalise au moment du publish.
working_data normalisée · audit complet Working data du back-office : tout ce que les opérateurs éditent. Chaque modification écrit aussi dans une table d'audit (qui a fait quoi, quand, depuis quelle IP).
published_data snapshot read-only · dénormalisée · versionnée Lecture seule pour l'API mobile. Données dénormalisées au moment de la publication pour éviter toute jointure en prod. Plusieurs versions peuvent cohabiter via un URL prefix versioning.
segment_events analytics · écritures massives Événements analytiques (taps, navigations, sessions). Isolée pour que les pics d'écriture n'aient aucun impact sur les tables métier.
Pourquoi une transaction distribuée
Quand un opérateur édite une entité métier, il faut écrire dans working_data ET dans la table d'audit en atomique. Si l'audit échoue, l'édition doit roll-back, et inversement. Une transaction locale par DB ne suffit pas — il faut une transaction distribuée qui s'étend sur plusieurs datasources.
Concrètement, un seul @Transactional dans le service métier englobe l'update sur working_data ET l'insertion d'une entrée d'audit. Le gestionnaire de transactions (Atomikos JTA, en l'occurrence) prend en charge le 2-phase commit entre les datasources.
L'alternative — XA natif PostgreSQL — était moins mature côté Hibernate à l'époque. Atomikos s'intègre proprement, mais attention : si un commit échoue partiellement, l'erreur peut être muette dans certains cas. On a appris à scruter les logs Atomikos et à ne jamais mélanger @Async et @Transactional (préférer un message JMS).
La publication, ou comment passer en read-only
Quand un opérateur valide un site, il faut copier les données métier de `working_data` vers `published_data` en les dénormalisant. C'est lourd (potentiellement plusieurs minutes pour un grand aéroport) et ça ne doit pas bloquer l'utilisateur. Le pattern :
- 1
L'opérateur clique « Publier » → l'API enregistre une demande dans une table de jobs et publie un message JMS sur Apache Artemis.
- 2
Un consumer Spring Batch reçoit le message, lance le job (reader/processor/writer). Quartz orchestre le scheduling et le clustering (un seul worker prend le job à la fois).
- 3
Le job copie + dénormalise vers
published_dataavec checkpoints. Si ça crash, il reprend où il s'est arrêté. - 4
Le statut bascule en
published. Les apps mobiles lisent la nouvelle version. L'ancienne reste en base pour rollback rapide.
Côté API mobile, l'URL prefixe la version (/mobile/api/vN/...). On peut publier une nouvelle version sans casser les anciennes apps — temps de grâce intégré.
Ce qu’on a aussi évalué
Single-DB + table partitioning
Suffisant tant que le pattern lecture/écriture reste proche. Insuffisant ici : la dénormalisation pour le mobile demande un schéma différent, pas juste un partitionnement.
Microservices séparés
Aurait surévalué le besoin. Trois bases dans un même monolithe Spring suffisent pour le volume actuel et restent simples à déployer (un seul artefact Maven).
Read replicas
Bonne idée pour scale en lecture mais ne résout pas la dénormalisation. Et le délai de réplication aurait introduit une fenêtre de cohérence inacceptable pour le workflow de publication.
Event sourcing
Tentant pour l'audit, mais surcoût important pour une équipe de notre taille. une table d'audit + transaction distribuée donne 80 % des bénéfices à 10 % du coût.
Ce qu’on retient
- Trois bases dans une même app, ce n’est pas trois fois plus de complexité — c’est moins, parce que chaque base a un rôle clair et un schéma optimisé pour ce rôle.
- Atomikos est solide tant qu'on évite les mélanges async / transactionnel. Tester les chemins d'erreur (mock d'une DB qui tombe) est obligatoire.
- Le versioning URL pour l'API mobile vaut largement les en-têtes : explicite, debuggable, et permet à plusieurs versions de cohabiter sans état partagé.
- Spring Batch + Quartz couvre 100 % des besoins de jobs longs. Kafka aurait été overkill pour notre volume.
- Ne **jamais** modifier une version d'API mobile déjà publiée. Toujours créer la version suivante. Les apps mobiles ont des cycles de mise à jour de plusieurs mois.