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 :
- écrire dans la table métier ET dans la table
outboxdans la même transaction - un processus séparé lit la table
outboxet publie vers Kafka
À retenir
@Transactionalgè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