# Colloquio Spring Boot: Propagazione delle Transazioni > Padroneggia la propagazione delle transazioni in Spring Boot: REQUIRED, REQUIRES_NEW, NESTED e altro. 12 domande di colloquio con codice e trappole comuni. - Published: 2026-03-18 - Updated: 2026-05-01 - Author: SharpSkill - Tags: spring boot, transactions, propagation, java, interview - Reading time: 14 min --- La propagazione delle transazioni rappresenta un concetto fondamentale di Spring Boot, regolarmente valutato durante i colloqui tecnici. Comprendere come le transazioni interagiscono tra metodi annotati con `@Transactional` aiuta a evitare bug sottili in produzione e consente di progettare architetture robuste. > **Consiglio di preparazione** > > Gli intervistatori valutano la capacità di scegliere il giusto livello di propagazione in base al contesto di business. Saper spiegare perché REQUIRES_NEW invece di REQUIRED in un caso specifico fa la differenza. ## Fondamenti della propagazione delle transazioni ### Domanda 1: Cos'è la propagazione delle transazioni in Spring? La propagazione definisce il comportamento di un metodo transazionale quando viene invocato all'interno del contesto di una transazione esistente. Risponde alla domanda: «Cosa succede quando un metodo `@Transactional` chiama un altro metodo anch'esso annotato?» ```java // OrderService.java // Dimostrazione del concetto di propagazione @Service public class OrderService { private final PaymentService paymentService; private final OrderRepository orderRepository; // Transazione padre - avvia una nuova transazione @Transactional public void createOrder(OrderRequest request) { // Salva l'ordine nella transazione corrente Order order = orderRepository.save(new Order(request)); // Chiamata a un altro metodo @Transactional // La propagazione determina: stessa transazione o nuova? paymentService.processPayment(order.getId(), request.getAmount()); } } @Service public class PaymentService { // Propagazione di default: REQUIRED // Si unisce alla transazione esistente di createOrder() @Transactional public void processPayment(Long orderId, BigDecimal amount) { // Esegue nella STESSA transazione di createOrder() // Se questo metodo fallisce, anche l'ordine viene rollback } } ``` Spring fornisce sette livelli di propagazione, ciascuno adatto a esigenze di business specifiche. La scelta impatta direttamente sulla coerenza dei dati e sulle performance. ### Domanda 2: Descrivi il comportamento di REQUIRED (propagazione di default) REQUIRED è la propagazione di default. Se esiste una transazione, il metodo vi si unisce. Altrimenti, viene creata una nuova transazione. È il comportamento più comune e intuitivo. ```java // UserService.java // REQUIRED: comportamento di default @Service public class UserService { private final UserRepository userRepository; private final AuditService auditService; @Transactional(propagation = Propagation.REQUIRED) public void updateUser(Long userId, UserUpdateRequest request) { // Avvia una transazione se non ne esiste alcuna User user = userRepository.findById(userId).orElseThrow(); user.setEmail(request.getEmail()); userRepository.save(user); // L'audit si unisce alla stessa transazione auditService.logUpdate(userId, "EMAIL_CHANGED"); } } @Service public class AuditService { private final AuditLogRepository auditLogRepository; @Transactional(propagation = Propagation.REQUIRED) public void logUpdate(Long userId, String action) { // Si unisce alla transazione di updateUser() // Commit o rollback insieme auditLogRepository.save(new AuditLog(userId, action, Instant.now())); } } ``` Il diagramma seguente illustra il flusso transazionale: ```text updateUser() avvia TX-1 ├── save(user) → TX-1 └── logUpdate() → si unisce a TX-1 (REQUIRED) └── save(audit) → TX-1 Se logUpdate() fallisce → rollback TX-1 → user E audit annullati ``` ### Domanda 3: Quando usare REQUIRES_NEW invece di REQUIRED? REQUIRES_NEW sospende la transazione esistente e crea una nuova transazione indipendente. Utile quando un'operazione deve essere committata indipendentemente dall'esito della transazione padre. ```java // PaymentService.java // REQUIRES_NEW: transazione indipendente @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 DEVE essere persistito anche se il pagamento fallisce dopo auditService.logPaymentAttempt(orderId, amount); // Simulazione di un errore dopo l'audit if (amount.compareTo(BigDecimal.ZERO) < 0) { throw new InvalidAmountException("Negative amount"); } } } @Service public class PaymentAuditService { private final PaymentAuditRepository auditRepository; // REQUIRES_NEW: committa indipendentemente dalla transazione padre @Transactional(propagation = Propagation.REQUIRES_NEW) public void logPaymentAttempt(Long orderId, BigDecimal amount) { // Nuova transazione TX-2 creata // TX-1 (processPayment) è sospesa auditRepository.save(new PaymentAudit(orderId, amount, "ATTEMPTED")); // TX-2 committa qui, indipendentemente da TX-1 } } ``` Il flusso transazionale con REQUIRES_NEW: ```text processPayment() avvia TX-1 ├── save(payment) → TX-1 ├── logPaymentAttempt() → TX-1 SOSPESA │ └── avvia TX-2 → nuova transazione │ └── save(audit) → TX-2 │ └── COMMIT TX-2 → audit persistito │ └── TX-1 RIPRENDE └── throw InvalidAmountException └── ROLLBACK TX-1 → pagamento annullato, ma audit conservato ``` > **Attenzione ai deadlock** > > REQUIRES_NEW può causare deadlock se la nuova transazione accede alle stesse risorse bloccate dalla transazione sospesa. Evita di usare REQUIRES_NEW per modificare le stesse tabelle della transazione padre. ### Domanda 4: Spiega la propagazione NESTED e in cosa differisce da REQUIRES_NEW NESTED crea un savepoint all'interno della transazione corrente. Se il metodo fallisce, vengono rollback solo le modifiche dal savepoint, non l'intera transazione padre. ```java // BatchProcessingService.java // NESTED: savepoint nella transazione padre @Service public class BatchProcessingService { private final ItemRepository itemRepository; private final ItemProcessor itemProcessor; @Transactional public BatchResult processBatch(List items) { BatchResult result = new BatchResult(); for (Item item : items) { try { // Ogni item è elaborato con un savepoint itemProcessor.processItem(item); result.addSuccess(item.getId()); } catch (ProcessingException e) { // Rollback solo di questo item, non dell'intero batch result.addFailure(item.getId(), e.getMessage()); } } return result; // Commit degli item riusciti } } @Service public class ItemProcessor { private final ItemRepository itemRepository; // NESTED: crea un savepoint, rollback parziale possibile @Transactional(propagation = Propagation.NESTED) public void processItem(Item item) { item.setStatus("PROCESSING"); itemRepository.save(item); // Validazione di business if (!isValid(item)) { throw new ProcessingException("Invalid item"); // Rollback al savepoint → solo questo item } item.setStatus("COMPLETED"); itemRepository.save(item); } } ``` Confronto NESTED vs REQUIRES_NEW: ```text NESTED: ├── Usa un savepoint nella TX padre ├── In caso di errore → rollback al savepoint ├── Se la TX padre fa rollback → anche NESTED viene rollback └── Più performante (nessuna nuova connessione) REQUIRES_NEW: ├── Crea una transazione completamente indipendente ├── In caso di errore → rollback solo della TX figlia ├── Se la TX padre fa rollback → la TX figlia HA GIÀ COMMITTATO └── Richiede una nuova connessione ``` > **Supporto NESTED** > > La propagazione NESTED richiede il supporto JDBC dei savepoint. La maggior parte dei database moderni (PostgreSQL, MySQL, Oracle) lo supporta. Verifica la compatibilità prima dell'uso. ## Tipi di propagazione avanzati ### Domanda 5: Quando usare SUPPORTS e NOT_SUPPORTED? SUPPORTS esegue all'interno della transazione esistente se presente, altrimenti senza transazione. NOT_SUPPORTED sospende qualsiasi transazione esistente ed esegue senza transazione. ```java // ReportingService.java // SUPPORTS: transazione opzionale @Service public class ReportingService { private final ReportRepository reportRepository; // SUPPORTS: funziona con o senza transazione // Utile per letture che non necessitano garanzie transazionali @Transactional(propagation = Propagation.SUPPORTS) public Report generateReport(Long reportId) { // Se chiamato da un metodo @Transactional → usa la sua TX // Se chiamato direttamente → nessuna transazione (lettura OK) return reportRepository.generateComplexReport(reportId); } } @Service public class ExternalNotificationService { private final ExternalApiClient apiClient; // NOT_SUPPORTED: mai dentro una transazione // Evita di bloccare la TX durante una chiamata esterna lenta @Transactional(propagation = Propagation.NOT_SUPPORTED) public void sendExternalNotification(String message) { // TX padre sospesa durante la chiamata apiClient.send(message); // Chiamata HTTP potenzialmente lenta // TX padre riprende dopo } } ``` ```java // OrderService.java // Esempio di uso combinato @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); // Generazione report nella stessa TX (SUPPORTS) Report report = reportingService.generateReport(orderId); // Notifica esterna FUORI dalla transazione (NOT_SUPPORTED) // Evita timeout della TX se l'API esterna è lenta notificationService.sendExternalNotification( "Order " + orderId + " completed" ); } } ``` ### Domanda 6: Spiega MANDATORY e NEVER MANDATORY richiede una transazione esistente e lancia un'eccezione altrimenti. NEVER richiede l'assenza di transazione e lancia un'eccezione se ne esiste una. ```java // AuditService.java // MANDATORY: deve essere chiamato dall'interno di una transazione @Service public class AuditService { private final AuditRepository auditRepository; // MANDATORY: rifiuta di eseguire senza una transazione esistente // Garantisce che l'audit sia sempre atomico con l'operazione auditata @Transactional(propagation = Propagation.MANDATORY) public void logCriticalAction(String action, Long entityId) { // Se chiamato senza transazione → IllegalTransactionStateException auditRepository.save(new AuditLog(action, entityId, Instant.now())); } } // CacheService.java // NEVER: non deve mai essere in una transazione @Service public class CacheService { private final CacheManager cacheManager; // NEVER: la cache non deve partecipare alle transazioni // Evita incoerenze tra cache e DB dopo un rollback @Transactional(propagation = Propagation.NEVER) public void invalidateCache(String cacheKey) { // Se chiamato da un @Transactional → IllegalTransactionStateException cacheManager.getCache("entities").evict(cacheKey); } } ``` ```java // SecurityService.java // Uso corretto di MANDATORY @Service public class SecurityService { private final AuditService auditService; @Transactional public void changeUserPassword(Long userId, String newPassword) { // Operazione sensibile... updatePassword(userId, newPassword); // L'audit DEVE essere nella stessa transazione // MANDATORY impone questo vincolo architetturale auditService.logCriticalAction("PASSWORD_CHANGE", userId); } // ERRORE: chiamata diretta senza transazione public void badUsage() { // Lancia IllegalTransactionStateException perché manca la TX auditService.logCriticalAction("TEST", 1L); } } ``` ## Trappole comuni nei colloqui ### Domanda 7: Perché @Transactional non funziona sulle chiamate interne? Una delle trappole più comuni. Le chiamate interne ai metodi (self-invocation) bypassano il proxy Spring, disabilitando la gestione transazionale. ```java // BrokenService.java // ERRORE CLASSICO: self-invocation @Service public class BrokenService { private final ItemRepository itemRepository; public void processItems(List itemIds) { for (Long id : itemIds) { // TRAPPOLA: chiamata interna → bypassa il proxy // @Transactional su processItem() viene IGNORATO processItem(id); } } @Transactional public void processItem(Long itemId) { // Questa transazione NON viene MAI creata nelle chiamate interne Item item = itemRepository.findById(itemId).orElseThrow(); item.setStatus("PROCESSED"); itemRepository.save(item); } } ``` Soluzioni per evitare questa trappola: ```java // Soluzione 1: Self-injection @Service public class FixedServiceWithSelfInjection { private final ItemRepository itemRepository; @Lazy @Autowired private FixedServiceWithSelfInjection self; // Self-injection public void processItems(List itemIds) { for (Long id : itemIds) { // Chiamata tramite proxy → @Transactional funziona self.processItem(id); } } @Transactional public void processItem(Long itemId) { // Transazione correttamente creata Item item = itemRepository.findById(itemId).orElseThrow(); item.setStatus("PROCESSED"); itemRepository.save(item); } } // Soluzione 2: Separare in due servizi (consigliata) @Service public class ItemOrchestrator { private final ItemProcessor processor; public void processItems(List itemIds) { for (Long id : itemIds) { // Chiamata a un altro bean → il proxy funziona processor.processItem(id); } } } @Service public class ItemProcessor { private final ItemRepository itemRepository; @Transactional public void processItem(Long itemId) { // Transazione gestita correttamente Item item = itemRepository.findById(itemId).orElseThrow(); item.setStatus("PROCESSED"); itemRepository.save(item); } } ``` ### Domanda 8: Come gestire eccezioni e rollback? Di default, Spring fa rollback solo su RuntimeException ed Error. Le checked exception NON attivano il rollback automatico. ```java // TransactionRollbackDemo.java // Comportamento di rollback in base al tipo di eccezione @Service public class TransactionRollbackDemo { private final OrderRepository orderRepository; // RuntimeException → ROLLBACK automatico @Transactional public void methodWithRuntimeException() { orderRepository.save(new Order()); throw new RuntimeException("Error"); // ROLLBACK } // Checked Exception → NESSUN rollback di default @Transactional public void methodWithCheckedException() throws IOException { orderRepository.save(new Order()); throw new IOException("File error"); // Fa COMMIT comunque! } // Forzare il rollback su checked exception @Transactional(rollbackFor = IOException.class) public void methodWithRollbackFor() throws IOException { orderRepository.save(new Order()); throw new IOException("Error"); // ROLLBACK grazie a rollbackFor } // Escludere una RuntimeException dal rollback @Transactional(noRollbackFor = BusinessException.class) public void methodWithNoRollbackFor() { orderRepository.save(new Order()); throw new BusinessException("Warning"); // COMMIT nonostante l'eccezione } } ``` Configurazione consigliata per casi di business: ```java // BaseTransactionalService.java // Configurazione transazionale coerente @Service public abstract class BaseTransactionalService { // Rollback su tutte le eccezioni (checked e 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) { // NON fa rollback - si vuole conservare la traccia del tentativo throw new InsufficientFundsException("Insufficient balance"); } return new PaymentResult(payment.getId(), "SUCCESS"); } } ``` ### Domanda 9: Come funziona l'isolamento delle transazioni con la propagazione? Isolamento e propagazione sono complementari. L'isolamento determina la visibilità dei dati tra transazioni concorrenti. ```java // IsolationDemo.java // Combinazione isolamento + propagazione @Service public class IsolationDemo { private final AccountRepository accountRepository; // READ_COMMITTED: vede dati committati da altre transazioni @Transactional( isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED ) public BigDecimal getAccountBalance(Long accountId) { // Può vedere valori diversi se riletto durante la transazione return accountRepository.findById(accountId) .map(Account::getBalance) .orElse(BigDecimal.ZERO); } // REPEATABLE_READ: garantisce la stessa lettura durante la transazione @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(); // Anche se un'altra transazione modifica 'from' nel frattempo, // vediamo sempre il valore iniziale (snapshot) from.debit(amount); to.credit(amount); accountRepository.save(from); accountRepository.save(to); } // SERIALIZABLE: isolamento massimo, nessuna concorrenza @Transactional( isolation = Isolation.SERIALIZABLE, propagation = Propagation.REQUIRES_NEW ) public void criticalOperation(Long accountId) { // Blocca qualsiasi altra transazione su questi dati // Usare con parsimonia - impatto sulle performance Account account = accountRepository.findById(accountId).orElseThrow(); account.performCriticalUpdate(); accountRepository.save(account); } } ``` Tabella riassuntiva dei livelli di isolamento: ```text | Isolamento | Dirty Read | Non-Repeatable | Phantom | |------------------|------------|----------------|---------| | READ_UNCOMMITTED | Possibile | Possibile | Possibile| | READ_COMMITTED | No | Possibile | Possibile| | REPEATABLE_READ | No | No | Possibile| | SERIALIZABLE | No | No | No | ``` > **Impatto sulle performance** > > Più rigoroso è l'isolamento, più le performance possono essere influenzate dai lock. SERIALIZABLE può causare contesa significativa in produzione ad alto traffico. ## Pattern avanzati ### Domanda 10: Come implementare il pattern «transactional outbox»? Il pattern outbox garantisce coerenza tra le modifiche al database e l'invio di messaggi/eventi, anche in caso di guasto. ```java // OutboxService.java // Pattern Transactional Outbox @Service public class OutboxService { private final OutboxRepository outboxRepository; // Salva l'evento nella stessa transazione dell'entità di business @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) { // Creare l'ordine Order order = new Order(request); orderRepository.save(order); // Evento outbox nella STESSA transazione (MANDATORY) // Se il commit ha successo → entrambi vengono persistiti // Se rollback → nessuno viene persistito outboxService.saveEvent( "ORDER", order.getId(), "ORDER_CREATED", toJson(new OrderCreatedEvent(order)) ); return order; } } // OutboxPublisher.java // Processo separato che pubblica gli eventi @Service public class OutboxPublisher { private final OutboxRepository outboxRepository; private final MessageBroker messageBroker; // Transazione indipendente per ciascuna pubblicazione @Scheduled(fixedDelay = 1000) @Transactional(propagation = Propagation.REQUIRES_NEW) public void publishPendingEvents() { List 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); } } } ``` ### Domanda 11: Come testare i diversi comportamenti di propagazione? I test di propagazione richiedono attenzione particolare per verificare il comportamento transazionale atteso. ```java // TransactionPropagationTest.java // Test dei comportamenti di propagazione @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 - pagamento nella stessa transazione (REQUIRED) paymentService.processPayment(order.getId(), BigDecimal.TEN); // Then - entrambi visibili prima del 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 in REQUIRES_NEW orderId = orderService.createOrderWithAudit(new OrderRequest()); throw new RuntimeException("Simulated failure after audit"); } catch (RuntimeException e) { // Transazione principale fa rollback } // Then - audit (REQUIRES_NEW) è comunque committato assertThat(findAuditLog(orderId)).isNotNull(); // Ma l'ordine viene rollback assertThat(findOrder(orderId)).isNull(); } @Test void mandatory_shouldThrowWithoutTransaction() { // Given - nessuna transazione attiva // When/Then - deve lanciare un'eccezione assertThatThrownBy(() -> auditService.logCriticalAction("TEST", 1L)) .isInstanceOf(IllegalTransactionStateException.class) .hasMessageContaining("No existing transaction"); } } ``` ```java // PropagationIntegrationTest.java // Test di integrazione con rollback e commit reali @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 - elaborazione batch con NESTED BatchResult result = orderService.processBatchWithNested( List.of(validItem(), invalidItem(), validItem()) ); // Then - solo gli item validi vengono persistiti assertThat(result.getSuccessCount()).isEqualTo(2); assertThat(result.getFailureCount()).isEqualTo(1); assertThat(orderRepository.findAll().size()).isEqualTo(initialCount + 2); } } ``` ### Domanda 12: Quali sono le best practice per la configurazione delle transazioni? Una configurazione transazionale coerente a livello di progetto evita sorprese e facilita la manutenzione. ```java // TransactionConfig.java // Configurazione transazionale centralizzata @Configuration @EnableTransactionManagement public class TransactionConfig { // Timeout di default per tutte le transazioni @Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { DataSourceTransactionManager tm = new DataSourceTransactionManager(dataSource); tm.setDefaultTimeout(30); // 30 secondi massimo per transazione return tm; } } // BaseService.java // Annotazioni di default per i servizi @Service @Transactional( readOnly = true, // Sola lettura di default rollbackFor = Exception.class // Rollback su qualsiasi eccezione ) public abstract class BaseService { // I metodi di lettura ereditano readOnly = true } // OrderService.java // Servizio con configurazione coerente @Service public class OrderService extends BaseService { private final OrderRepository orderRepository; // Eredita readOnly = true public Order findById(Long id) { return orderRepository.findById(id).orElseThrow(); } // Override per le scritture @Transactional(readOnly = false) public Order createOrder(OrderRequest request) { return orderRepository.save(new Order(request)); } // Configurazione esplicita per casi critici @Transactional( readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE, timeout = 10 ) public void criticalOperation(Long orderId) { // Operazione con isolamento massimo e timeout breve } } ``` Checklist di best practice: ```text Configurazione Transazione - Checklist ✅ readOnly = true di default, override esplicito per le scritture ✅ rollbackFor = Exception.class per includere checked exception ✅ Timeout adeguato in base al tipo di operazione ✅ Evitare chiamate interne (@Transactional ignorato) ✅ REQUIRES_NEW solo quando un commit indipendente è necessario ✅ MANDATORY per garantire contesto transazionale ✅ Test espliciti dei comportamenti di rollback ✅ Monitorare le transazioni di lunga durata ✅ Documentare scelte di propagazione non standard ``` ## Conclusione La propagazione delle transazioni è un concetto fondamentale valutato nei colloqui Spring Boot. Punti chiave da ricordare: **Tipi di propagazione comuni:** - ✅ REQUIRED (default): si unisce o crea una transazione - ✅ REQUIRES_NEW: transazione indipendente, commit separato - ✅ NESTED: savepoint per rollback parziale - ✅ MANDATORY: richiede una transazione esistente **Trappole da evitare:** - ✅ Self-invocation: bypassa il proxy, @Transactional ignorato - ✅ Checked exception: nessun rollback di default - ✅ REQUIRES_NEW sugli stessi dati: rischio di deadlock - ✅ Timeout assente: transazioni bloccate indefinitamente **Best practice:** - ✅ readOnly = true di default - ✅ rollbackFor = Exception.class sistematicamente - ✅ Servizi separati per evitare self-invocation - ✅ Testare esplicitamente i comportamenti di rollback Padroneggiare la propagazione delle transazioni dimostra una comprensione profonda di Spring e della gestione dei dati. Questi concetti sono essenziali per progettare applicazioni robuste e superare con successo i colloqui tecnici. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/it/blog/spring-boot/spring-boot-transaction-propagation-interview