SB
articles
Article · Architecture 2026-05 · 10 min de lecture

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).

Le problème

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.

L'archi

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 :

Back-office édition
opérateurs métier
App mobile
lecture massive
REST · écritures
REST · lectures
Spring Boot 3 · Java 17
@Transactional · Atomikos JTA · Spring Batch · Quartz · Artemis JMS
CRUD sync
publish · job batch
events stream
working_data
normalisée
audit complet
published_data
dénormalisée
read-only · versionnée
segment_events
analytics
é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.

Atomicité

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).

Workflow

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. 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. 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. 3

    Le job copie + dénormalise vers published_data avec checkpoints. Si ça crash, il reprend où il s'est arrêté.

  4. 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é.

Choix

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.

Lessons

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.