Questions entretien Spring Boot : propagation des transactions expliquée

Maîtrisez la propagation des transactions Spring Boot : REQUIRED, REQUIRES_NEW, NESTED et plus. 12 questions d'entretien avec exemples de code et pièges courants.

Spring Boot Transaction Propagation : questions d'entretien et exemples pratiques

La propagation des transactions représente un concept fondamental dans Spring Boot, régulièrement évalué lors des entretiens techniques. Comprendre comment les transactions interagissent entre méthodes annotées @Transactional permet d'éviter des bugs subtils en production et de concevoir des architectures robustes.

Conseil de préparation

Les recruteurs testent la capacité à choisir le bon niveau de propagation selon le contexte métier. Savoir expliquer pourquoi REQUIRES_NEW plutôt que REQUIRED dans un cas précis fait la différence.

Fondamentaux de la propagation transactionnelle

Question 1 : Qu'est-ce que la propagation des transactions dans Spring ?

La propagation définit le comportement d'une méthode transactionnelle lorsqu'elle est appelée dans le contexte d'une transaction existante. Elle répond à la question : "Que se passe-t-il si une méthode @Transactional en appelle une autre également annotée ?"

OrderService.javajava
// Démonstration du concept de propagation
@Service
public class OrderService {

    private final PaymentService paymentService;
    private final OrderRepository orderRepository;

    // Transaction parent - démarre une nouvelle transaction
    @Transactional
    public void createOrder(OrderRequest request) {
        // Sauvegarde la commande dans la transaction courante
        Order order = orderRepository.save(new Order(request));

        // Appel d'une autre méthode @Transactional
        // La propagation détermine : même transaction ou nouvelle ?
        paymentService.processPayment(order.getId(), request.getAmount());
    }
}

@Service
public class PaymentService {

    // Propagation par défaut : REQUIRED
    // Rejoint la transaction existante de createOrder()
    @Transactional
    public void processPayment(Long orderId, BigDecimal amount) {
        // S'exécute dans la MÊME transaction que createOrder()
        // Si cette méthode échoue, la commande est également rollback
    }
}

Spring propose sept niveaux de propagation, chacun adapté à des besoins métier spécifiques. Le choix impacte directement la cohérence des données et les performances.

Question 2 : Décrivez le comportement de REQUIRED (propagation par défaut)

REQUIRED est la propagation par défaut. Si une transaction existe, la méthode la rejoint. Sinon, une nouvelle transaction est créée. C'est le comportement le plus courant et le plus intuitif.

UserService.javajava
// REQUIRED : comportement par défaut
@Service
public class UserService {

    private final UserRepository userRepository;
    private final AuditService auditService;

    @Transactional(propagation = Propagation.REQUIRED)
    public void updateUser(Long userId, UserUpdateRequest request) {
        // Démarre une transaction si aucune n'existe
        User user = userRepository.findById(userId).orElseThrow();
        user.setEmail(request.getEmail());
        userRepository.save(user);

        // L'audit rejoint la même transaction
        auditService.logUpdate(userId, "EMAIL_CHANGED");
    }
}

@Service
public class AuditService {

    private final AuditLogRepository auditLogRepository;

    @Transactional(propagation = Propagation.REQUIRED)
    public void logUpdate(Long userId, String action) {
        // Rejoint la transaction de updateUser()
        // Commit ou rollback ensemble
        auditLogRepository.save(new AuditLog(userId, action, Instant.now()));
    }
}

Le diagramme ci-dessous illustre le flux transactionnel :

text
updateUser() démarre TX-1
    ├── save(user)          → TX-1
    └── logUpdate()         → rejoint TX-1 (REQUIRED)
           └── save(audit)  → TX-1

Si logUpdate() échoue → TX-1 rollback → user ET audit annulés

Question 3 : Quand utiliser REQUIRES_NEW au lieu de REQUIRED ?

REQUIRES_NEW suspend la transaction existante et crée une nouvelle transaction indépendante. Utile quand une opération doit être commitée indépendamment du résultat de la transaction parent.

PaymentService.javajava
// REQUIRES_NEW : transaction indépendante
@Service
public class PaymentService {

    private final PaymentRepository paymentRepository;
    private final PaymentAuditService auditService;

    @Transactional
    public void processPayment(Long orderId, BigDecimal amount) {
        Payment payment = new Payment(orderId, amount);
        paymentRepository.save(payment);

        // L'audit DOIT être persisté même si le paiement échoue ensuite
        auditService.logPaymentAttempt(orderId, amount);

        // Simulation d'une erreur après l'audit
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new InvalidAmountException("Montant négatif");
        }
    }
}

@Service
public class PaymentAuditService {

    private final PaymentAuditRepository auditRepository;

    // REQUIRES_NEW : commit indépendant de la transaction parent
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logPaymentAttempt(Long orderId, BigDecimal amount) {
        // Nouvelle transaction TX-2 créée
        // TX-1 (processPayment) est suspendue
        auditRepository.save(new PaymentAudit(orderId, amount, "ATTEMPTED"));
        // TX-2 commit ici, indépendamment de TX-1
    }
}

Le flux transactionnel avec REQUIRES_NEW :

text
processPayment() démarre TX-1
    ├── save(payment)              → TX-1
    ├── logPaymentAttempt()        → TX-1 SUSPENDUE
    │       └── démarre TX-2       → nouvelle transaction
    │       └── save(audit)        → TX-2
    │       └── TX-2 COMMIT        → audit persisté
    │       └── TX-1 REPREND
    └── throw InvalidAmountException
        └── TX-1 ROLLBACK          → payment annulé, mais audit conservé
Attention aux deadlocks

REQUIRES_NEW peut causer des deadlocks si la nouvelle transaction accède aux mêmes ressources verrouillées par la transaction suspendue. Éviter d'utiliser REQUIRES_NEW pour modifier les mêmes tables que la transaction parent.

Question 4 : Expliquez la propagation NESTED et sa différence avec REQUIRES_NEW

NESTED crée un savepoint dans la transaction courante. Si la méthode échoue, seules les modifications depuis le savepoint sont annulées, pas toute la transaction parent.

BatchProcessingService.javajava
// NESTED : savepoint dans la transaction parent
@Service
public class BatchProcessingService {

    private final ItemRepository itemRepository;
    private final ItemProcessor itemProcessor;

    @Transactional
    public BatchResult processBatch(List<Item> items) {
        BatchResult result = new BatchResult();

        for (Item item : items) {
            try {
                // Chaque item est traité avec un savepoint
                itemProcessor.processItem(item);
                result.addSuccess(item.getId());
            } catch (ProcessingException e) {
                // Rollback uniquement cet item, pas tout le batch
                result.addFailure(item.getId(), e.getMessage());
            }
        }

        return result; // Commit des items réussis
    }
}

@Service
public class ItemProcessor {

    private final ItemRepository itemRepository;

    // NESTED : crée un savepoint, rollback partiel possible
    @Transactional(propagation = Propagation.NESTED)
    public void processItem(Item item) {
        item.setStatus("PROCESSING");
        itemRepository.save(item);

        // Validation métier
        if (!isValid(item)) {
            throw new ProcessingException("Item invalide");
            // Rollback jusqu'au savepoint → cet item uniquement
        }

        item.setStatus("COMPLETED");
        itemRepository.save(item);
    }
}

Comparaison NESTED vs REQUIRES_NEW :

text
NESTED :
├── Utilise un savepoint dans TX parent
├── Si échec → rollback au savepoint
├── Si TX parent rollback → NESTED aussi rollback
└── Plus performant (pas de nouvelle connexion)

REQUIRES_NEW :
├── Crée une transaction complètement indépendante
├── Si échec → rollback TX enfant uniquement
├── Si TX parent rollback → TX enfant DÉJÀ COMMITÉE
└── Nouvelle connexion nécessaire
Support NESTED

La propagation NESTED nécessite un support JDBC de savepoints. La plupart des bases modernes (PostgreSQL, MySQL, Oracle) le supportent. Vérifier la compatibilité avant utilisation.

Propagations avancées

Question 5 : Quand utiliser SUPPORTS et NOT_SUPPORTED ?

SUPPORTS exécute dans la transaction existante si présente, sinon sans transaction. NOT_SUPPORTED suspend toute transaction existante et exécute sans transaction.

ReportingService.javajava
// SUPPORTS : transaction optionnelle
@Service
public class ReportingService {

    private final ReportRepository reportRepository;

    // SUPPORTS : fonctionne avec ou sans transaction
    // Utile pour les lectures qui n'ont pas besoin de garanties transactionnelles
    @Transactional(propagation = Propagation.SUPPORTS)
    public Report generateReport(Long reportId) {
        // Si appelé depuis une méthode @Transactional → utilise sa TX
        // Si appelé directement → pas de transaction (lecture seule OK)
        return reportRepository.generateComplexReport(reportId);
    }
}

@Service
public class ExternalNotificationService {

    private final ExternalApiClient apiClient;

    // NOT_SUPPORTED : jamais dans une transaction
    // Évite de bloquer la TX pendant un appel externe lent
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void sendExternalNotification(String message) {
        // TX parente suspendue pendant l'appel
        apiClient.send(message); // Appel HTTP potentiellement lent
        // TX parente reprend après
    }
}
OrderService.javajava
// Exemple d'utilisation combinée
@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final ReportingService reportingService;
    private final ExternalNotificationService notificationService;

    @Transactional
    public void completeOrder(Long orderId) {
        Order order = orderRepository.findById(orderId).orElseThrow();
        order.setStatus("COMPLETED");
        orderRepository.save(order);

        // Génération du rapport dans la même TX (SUPPORTS)
        Report report = reportingService.generateReport(orderId);

        // Notification externe HORS transaction (NOT_SUPPORTED)
        // Évite timeout de la TX si l'API externe est lente
        notificationService.sendExternalNotification(
            "Order " + orderId + " completed"
        );
    }
}

Question 6 : Expliquez MANDATORY et NEVER

MANDATORY exige une transaction existante et lance une exception sinon. NEVER exige l'absence de transaction et lance une exception si une existe.

AuditService.javajava
// MANDATORY : doit être appelé depuis une transaction
@Service
public class AuditService {

    private final AuditRepository auditRepository;

    // MANDATORY : refuse de s'exécuter sans transaction existante
    // Garantit que l'audit est toujours atomique avec l'opération auditée
    @Transactional(propagation = Propagation.MANDATORY)
    public void logCriticalAction(String action, Long entityId) {
        // Si appelé sans transaction → IllegalTransactionStateException
        auditRepository.save(new AuditLog(action, entityId, Instant.now()));
    }
}

// CacheService.java
// NEVER : ne doit jamais être dans une transaction
@Service
public class CacheService {

    private final CacheManager cacheManager;

    // NEVER : le cache ne doit pas participer aux transactions
    // Évite les incohérences entre cache et BDD après rollback
    @Transactional(propagation = Propagation.NEVER)
    public void invalidateCache(String cacheKey) {
        // Si appelé depuis une @Transactional → IllegalTransactionStateException
        cacheManager.getCache("entities").evict(cacheKey);
    }
}
SecurityService.javajava
// Utilisation correcte de MANDATORY
@Service
public class SecurityService {

    private final AuditService auditService;

    @Transactional
    public void changeUserPassword(Long userId, String newPassword) {
        // Opération sensible...
        updatePassword(userId, newPassword);

        // L'audit DOIT être dans la même transaction
        // MANDATORY garantit cette contrainte architecturale
        auditService.logCriticalAction("PASSWORD_CHANGE", userId);
    }

    // ERREUR : appel direct sans transaction
    public void badUsage() {
        // Lance IllegalTransactionStateException car pas de TX
        auditService.logCriticalAction("TEST", 1L);
    }
}

Prêt à réussir tes entretiens Spring Boot ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Pièges courants en entretien

Question 7 : Pourquoi @Transactional ne fonctionne pas sur les appels internes ?

L'un des pièges les plus fréquents. Les appels de méthode internes (self-invocation) contournent le proxy Spring, désactivant la gestion transactionnelle.

BrokenService.javajava
// ERREUR CLASSIQUE : self-invocation
@Service
public class BrokenService {

    private final ItemRepository itemRepository;

    public void processItems(List<Long> itemIds) {
        for (Long id : itemIds) {
            // PIÈGE : appel interne → contourne le proxy
            // @Transactional de processItem() est IGNORÉ
            processItem(id);
        }
    }

    @Transactional
    public void processItem(Long itemId) {
        // Cette transaction n'est JAMAIS créée lors d'un appel interne
        Item item = itemRepository.findById(itemId).orElseThrow();
        item.setStatus("PROCESSED");
        itemRepository.save(item);
    }
}

Solutions pour éviter ce piège :

java
// Solution 1 : Injection de self
@Service
public class FixedServiceWithSelfInjection {

    private final ItemRepository itemRepository;

    @Lazy
    @Autowired
    private FixedServiceWithSelfInjection self; // Auto-injection

    public void processItems(List<Long> itemIds) {
        for (Long id : itemIds) {
            // Appel via le proxy → @Transactional fonctionne
            self.processItem(id);
        }
    }

    @Transactional
    public void processItem(Long itemId) {
        // Transaction correctement créée
        Item item = itemRepository.findById(itemId).orElseThrow();
        item.setStatus("PROCESSED");
        itemRepository.save(item);
    }
}

// Solution 2 : Séparer en deux services (recommandé)
@Service
public class ItemOrchestrator {

    private final ItemProcessor processor;

    public void processItems(List<Long> itemIds) {
        for (Long id : itemIds) {
            // Appel sur un autre bean → proxy fonctionne
            processor.processItem(id);
        }
    }
}

@Service
public class ItemProcessor {

    private final ItemRepository itemRepository;

    @Transactional
    public void processItem(Long itemId) {
        // Transaction correctement gérée
        Item item = itemRepository.findById(itemId).orElseThrow();
        item.setStatus("PROCESSED");
        itemRepository.save(item);
    }
}

Question 8 : Comment gérer les exceptions et le rollback ?

Par défaut, Spring rollback uniquement sur les RuntimeException et Error. Les checked exceptions ne déclenchent PAS de rollback automatique.

TransactionRollbackDemo.javajava
// Comportement du rollback selon le type d'exception
@Service
public class TransactionRollbackDemo {

    private final OrderRepository orderRepository;

    // RuntimeException → ROLLBACK automatique
    @Transactional
    public void methodWithRuntimeException() {
        orderRepository.save(new Order());
        throw new RuntimeException("Erreur"); // ROLLBACK
    }

    // Checked Exception → PAS de rollback par défaut
    @Transactional
    public void methodWithCheckedException() throws IOException {
        orderRepository.save(new Order());
        throw new IOException("Erreur fichier"); // COMMIT quand même !
    }

    // Forcer le rollback sur checked exception
    @Transactional(rollbackFor = IOException.class)
    public void methodWithRollbackFor() throws IOException {
        orderRepository.save(new Order());
        throw new IOException("Erreur"); // ROLLBACK grâce à rollbackFor
    }

    // Exclure une RuntimeException du rollback
    @Transactional(noRollbackFor = BusinessException.class)
    public void methodWithNoRollbackFor() {
        orderRepository.save(new Order());
        throw new BusinessException("Warning"); // COMMIT malgré l'exception
    }
}

Configuration recommandée pour les cas métier :

BaseTransactionalService.javajava
// Configuration transactionnelle cohérente
@Service
public abstract class BaseTransactionalService {

    // Rollback sur toutes les exceptions (checked et unchecked)
    @Transactional(rollbackFor = Exception.class)
    protected void executeInTransaction(Runnable operation) {
        operation.run();
    }
}

// PaymentService.java
@Service
public class PaymentService extends BaseTransactionalService {

    private final PaymentRepository paymentRepository;

    @Transactional(
        rollbackFor = Exception.class,
        noRollbackFor = InsufficientFundsException.class
    )
    public PaymentResult processPayment(PaymentRequest request) {
        Payment payment = new Payment(request);
        paymentRepository.save(payment);

        if (request.getAmount().compareTo(getBalance()) > 0) {
            // Ne rollback PAS - on veut garder la trace de la tentative
            throw new InsufficientFundsException("Solde insuffisant");
        }

        return new PaymentResult(payment.getId(), "SUCCESS");
    }
}

Question 9 : Comment fonctionne l'isolation transactionnelle avec la propagation ?

L'isolation et la propagation sont complémentaires. L'isolation détermine la visibilité des données entre transactions concurrentes.

IsolationDemo.javajava
// Combinaison isolation + propagation
@Service
public class IsolationDemo {

    private final AccountRepository accountRepository;

    // READ_COMMITTED : voit les données commitées par d'autres transactions
    @Transactional(
        isolation = Isolation.READ_COMMITTED,
        propagation = Propagation.REQUIRED
    )
    public BigDecimal getAccountBalance(Long accountId) {
        // Peut voir des valeurs différentes si relu pendant la transaction
        return accountRepository.findById(accountId)
            .map(Account::getBalance)
            .orElse(BigDecimal.ZERO);
    }

    // REPEATABLE_READ : garantit la même lecture pendant la transaction
    @Transactional(
        isolation = Isolation.REPEATABLE_READ,
        propagation = Propagation.REQUIRED
    )
    public void transferWithConsistentRead(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepository.findById(fromId).orElseThrow();
        Account to = accountRepository.findById(toId).orElseThrow();

        // Même si une autre transaction modifie 'from' entre-temps,
        // on voit toujours la valeur initiale (snapshot)
        from.debit(amount);
        to.credit(amount);

        accountRepository.save(from);
        accountRepository.save(to);
    }

    // SERIALIZABLE : isolation maximale, pas de concurrence
    @Transactional(
        isolation = Isolation.SERIALIZABLE,
        propagation = Propagation.REQUIRES_NEW
    )
    public void criticalOperation(Long accountId) {
        // Bloque toute autre transaction sur ces données
        // Utiliser avec parcimonie - impact performance
        Account account = accountRepository.findById(accountId).orElseThrow();
        account.performCriticalUpdate();
        accountRepository.save(account);
    }
}

Tableau récapitulatif des niveaux d'isolation :

text
| Isolation        | Dirty Read | Non-Repeatable | Phantom |
|------------------|------------|----------------|---------|
| READ_UNCOMMITTED | Possible   | Possible       | Possible|
| READ_COMMITTED   | Non        | Possible       | Possible|
| REPEATABLE_READ  | Non        | Non            | Possible|
| SERIALIZABLE     | Non        | Non            | Non     |
Impact sur les performances

Plus l'isolation est stricte, plus les performances peuvent être impactées par les verrous. SERIALIZABLE peut causer des contentions importantes en environnement de production à fort trafic.

Patterns avancés

Question 10 : Comment implémenter le pattern "transaction outbox" ?

Le pattern outbox garantit la cohérence entre les modifications en base et l'envoi de messages/événements, même en cas de panne.

OutboxService.javajava
// Pattern Transactional Outbox
@Service
public class OutboxService {

    private final OutboxRepository outboxRepository;

    // Sauvegarde l'événement dans la même transaction que l'entité métier
    @Transactional(propagation = Propagation.MANDATORY)
    public void saveEvent(String aggregateType, Long aggregateId, String eventType, String payload) {
        OutboxEvent event = OutboxEvent.builder()
            .aggregateType(aggregateType)
            .aggregateId(aggregateId)
            .eventType(eventType)
            .payload(payload)
            .status("PENDING")
            .createdAt(Instant.now())
            .build();
        outboxRepository.save(event);
    }
}

@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final OutboxService outboxService;

    @Transactional
    public Order createOrder(OrderRequest request) {
        // Création de la commande
        Order order = new Order(request);
        orderRepository.save(order);

        // Événement outbox dans la MÊME transaction (MANDATORY)
        // Si commit réussit → les deux sont persistés
        // Si rollback → aucun n'est persisté
        outboxService.saveEvent(
            "ORDER",
            order.getId(),
            "ORDER_CREATED",
            toJson(new OrderCreatedEvent(order))
        );

        return order;
    }
}

// OutboxPublisher.java
// Processus séparé qui publie les événements
@Service
public class OutboxPublisher {

    private final OutboxRepository outboxRepository;
    private final MessageBroker messageBroker;

    // Transaction indépendante pour chaque publication
    @Scheduled(fixedDelay = 1000)
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void publishPendingEvents() {
        List<OutboxEvent> events = outboxRepository
            .findByStatusOrderByCreatedAt("PENDING");

        for (OutboxEvent event : events) {
            try {
                messageBroker.publish(event.getEventType(), event.getPayload());
                event.setStatus("PUBLISHED");
                event.setPublishedAt(Instant.now());
            } catch (Exception e) {
                event.setStatus("FAILED");
                event.setError(e.getMessage());
            }
            outboxRepository.save(event);
        }
    }
}

Question 11 : Comment tester les différentes propagations ?

Les tests de propagation nécessitent une attention particulière pour vérifier le comportement transactionnel attendu.

TransactionPropagationTest.javajava
// Tests des comportements de propagation
@SpringBootTest
@Transactional
class TransactionPropagationTest {

    @Autowired
    private OrderService orderService;

    @Autowired
    private PaymentService paymentService;

    @Autowired
    private TestEntityManager entityManager;

    @Test
    void required_shouldShareTransaction() {
        // Given
        Order order = orderService.createOrder(new OrderRequest());

        // When - payment dans la même transaction (REQUIRED)
        paymentService.processPayment(order.getId(), BigDecimal.TEN);

        // Then - les deux sont visibles avant commit
        entityManager.flush();
        assertThat(entityManager.find(Order.class, order.getId())).isNotNull();
        assertThat(entityManager.find(Payment.class, order.getId())).isNotNull();
    }

    @Test
    void requiresNew_shouldCommitIndependently() {
        // Given
        Long orderId = null;

        try {
            // When - audit dans REQUIRES_NEW
            orderId = orderService.createOrderWithAudit(new OrderRequest());
            throw new RuntimeException("Simulated failure after audit");
        } catch (RuntimeException e) {
            // Transaction principale rollback
        }

        // Then - l'audit (REQUIRES_NEW) est quand même commitée
        assertThat(findAuditLog(orderId)).isNotNull();
        // Mais la commande est rollback
        assertThat(findOrder(orderId)).isNull();
    }

    @Test
    void mandatory_shouldThrowWithoutTransaction() {
        // Given - aucune transaction active

        // When/Then - doit lancer une exception
        assertThatThrownBy(() -> auditService.logCriticalAction("TEST", 1L))
            .isInstanceOf(IllegalTransactionStateException.class)
            .hasMessageContaining("No existing transaction");
    }
}
PropagationIntegrationTest.javajava
// Test d'intégration avec rollback et commit réels
@SpringBootTest
class PropagationIntegrationTest {

    @Autowired
    private OrderService orderService;

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private AuditLogRepository auditLogRepository;

    @Test
    void nested_shouldRollbackOnlyNestedOnFailure() {
        // Given
        int initialCount = orderRepository.findAll().size();

        // When - traitement batch avec NESTED
        BatchResult result = orderService.processBatchWithNested(
            List.of(validItem(), invalidItem(), validItem())
        );

        // Then - seuls les items valides sont persistés
        assertThat(result.getSuccessCount()).isEqualTo(2);
        assertThat(result.getFailureCount()).isEqualTo(1);
        assertThat(orderRepository.findAll().size()).isEqualTo(initialCount + 2);
    }
}

Question 12 : Quelles sont les bonnes pratiques de configuration transactionnelle ?

Une configuration transactionnelle cohérente au niveau du projet évite les surprises et facilite la maintenance.

TransactionConfig.javajava
// Configuration centralisée des transactions
@Configuration
@EnableTransactionManagement
public class TransactionConfig {

    // Timeout par défaut pour toutes les transactions
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        DataSourceTransactionManager tm = new DataSourceTransactionManager(dataSource);
        tm.setDefaultTimeout(30); // 30 secondes max par transaction
        return tm;
    }
}

// BaseService.java
// Annotations par défaut pour les services
@Service
@Transactional(
    readOnly = true, // Lecture seule par défaut
    rollbackFor = Exception.class // Rollback sur toute exception
)
public abstract class BaseService {
    // Méthodes de lecture héritent de readOnly = true
}

// OrderService.java
// Service avec configuration cohérente
@Service
public class OrderService extends BaseService {

    private final OrderRepository orderRepository;

    // Hérite de readOnly = true
    public Order findById(Long id) {
        return orderRepository.findById(id).orElseThrow();
    }

    // Override pour les écritures
    @Transactional(readOnly = false)
    public Order createOrder(OrderRequest request) {
        return orderRepository.save(new Order(request));
    }

    // Configuration explicite pour les cas critiques
    @Transactional(
        readOnly = false,
        propagation = Propagation.REQUIRES_NEW,
        isolation = Isolation.SERIALIZABLE,
        timeout = 10
    )
    public void criticalOperation(Long orderId) {
        // Opération avec isolation maximale et timeout court
    }
}

Checklist des bonnes pratiques :

text
Configuration transactionnelle - Checklist

✅ readOnly = true par défaut, override explicite pour les écritures
✅ rollbackFor = Exception.class pour inclure les checked exceptions
✅ Timeout approprié selon le type d'opération
✅ Éviter les appels internes (@Transactional ignoré)
✅ REQUIRES_NEW uniquement si commit indépendant nécessaire
✅ MANDATORY pour garantir le contexte transactionnel
✅ Tests explicites des comportements de rollback
✅ Monitoring des transactions longues
✅ Documentation des choix de propagation non-standard

Prêt à réussir tes entretiens Spring Boot ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Conclusion

La propagation des transactions est un concept fondamental évalué dans les entretiens Spring Boot. Les points essentiels à retenir :

Propagations courantes :

  • ✅ REQUIRED (défaut) : rejoint ou crée une transaction
  • ✅ REQUIRES_NEW : transaction indépendante, commit séparé
  • ✅ NESTED : savepoint pour rollback partiel
  • ✅ MANDATORY : exige une transaction existante

Pièges à éviter :

  • ✅ Self-invocation : contourne le proxy, @Transactional ignoré
  • ✅ Checked exceptions : pas de rollback par défaut
  • ✅ REQUIRES_NEW sur mêmes données : risque de deadlock
  • ✅ Timeout absent : transactions bloquées indéfiniment

Bonnes pratiques :

  • ✅ readOnly = true par défaut
  • ✅ rollbackFor = Exception.class systématique
  • ✅ Séparer les services pour éviter self-invocation
  • ✅ Tester explicitement les comportements de rollback

La maîtrise de la propagation transactionnelle démontre une compréhension approfondie de Spring et de la gestion des données. Ces concepts sont incontournables pour concevoir des applications robustes et passer les entretiens techniques avec succès.

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

#spring boot
#transactions
#propagation
#java
#entretien technique

Partager

Articles similaires