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.

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.
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 ?"
// 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.
// 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 :
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ésQuestion 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.
// 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 :
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é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.
// 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 :
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écessaireLa 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.
// 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
}
}// 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.
// 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);
}
}// 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.
// 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 :
// 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.
// 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 :
// 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.
// 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 :
| 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 |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.
// 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.
// 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");
}
}// 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.
// 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 :
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-standardPrê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
Partager
Articles similaires

Spring Batch 5 en entretien technique : partitioning, chunks et fault tolerance
Préparez vos entretiens Spring Batch 5 : 15 questions essentielles sur le partitioning, chunk-oriented processing, fault tolerance avec exemples de code Java 21.

Spring Modulith : architecture modulaire monolithique expliquée
Découvrez Spring Modulith pour construire des monolithes modulaires en Java. Architecture, modules, événements asynchrones et tests avec exemples Spring Boot 3.

Spring Security 6 : Authentification JWT complète
Guide pratique pour implémenter une authentification JWT avec Spring Security 6. Configuration, génération de tokens, validation et bonnes pratiques.