Aller au contenu

Transaction Patterns

Contexte

Une transaction regroupe plusieurs opérations en une unité atomique : soit tout réussit, soit tout est annulé. En architecture distribuée, la gestion des transactions devient complexe.


Transaction locale

Une seule base de données, gérée par le SGBD :

@Transactional
public void createOrder(OrderRequest request) {
    Order order = orderRepository.save(new Order(request));
    inventoryRepository.decrementStock(request.getProductId(), request.getQuantity());
    // Si une exception est levée, tout est rollback
}

Le SGBD garantit les propriétés ACID pour les transactions locales.


Niveaux d'isolation

Niveau Dirty Read Non-repeatable Read Phantom Read
READ UNCOMMITTED Possible Possible Possible
READ COMMITTED Non Possible Possible
REPEATABLE READ Non Non Possible
SERIALIZABLE Non Non Non

PostgreSQL utilise READ COMMITTED par défaut. SERIALIZABLE offre la plus forte garantie mais réduit la concurrence.

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void processPayment(Long orderId) {
    // ...
}

Propagation des transactions (Spring)

Type Comportement
REQUIRED (défaut) Rejoint la transaction existante ou en crée une nouvelle
REQUIRES_NEW Crée toujours une nouvelle transaction (suspend l'existante)
NESTED Crée un savepoint dans la transaction existante
SUPPORTS Utilise la transaction si elle existe, sinon exécute sans
NOT_SUPPORTED Exécute sans transaction (suspend l'existante)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void sendNotification(Order order) {
    // Transaction indépendante — un échec ici ne rollback pas la commande
}

Problèmes courants

Lost Update

Deux transactions lisent puis écrivent la même donnée — la dernière écriture écrase la première.

Solution : optimistic locking avec @Version.

N+1 queries dans une transaction

List<Order> orders = orderRepository.findAll();
for (Order order : orders) {
    order.getItems().size(); // N requêtes supplémentaires
}

Solution : JOIN FETCH ou @EntityGraph.

Transaction trop longue

Une transaction qui inclut des appels HTTP ou des traitements longs garde un lock en base.

Solution : limiter la transaction aux opérations base de données, externaliser le reste.


Transactions distribuées

Quand plusieurs services ont chacun leur base de données, une transaction locale ne suffit plus.

Saga Pattern

Séquence de transactions locales avec compensation :

Commande créée → Paiement effectué → Stock réservé
                                       ↓ (échec)
                                   Paiement annulé ← Compensation

Deux approches :

  • Choreography — chaque service écoute les événements et réagit
  • Orchestration — un coordinateur central pilote la séquence

Outbox Pattern

Garantit qu'un événement est publié si et seulement si la transaction locale réussit :

  1. écrire dans la table métier ET dans la table outbox dans la même transaction
  2. un processus séparé lit la table outbox et publie vers Kafka

À retenir

  • @Transactional gère les transactions locales avec Spring
  • le niveau d'isolation par défaut (READ COMMITTED) suffit dans la plupart des cas
  • ne jamais inclure d'appels HTTP ou de traitements longs dans une transaction
  • en distribué, utiliser le saga pattern avec compensation
  • l'outbox pattern garantit la cohérence entre la base et le broker

Voir aussi