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.

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.
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?»
// 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.
// 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:
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 annullatiDomanda 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.
// 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:
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 conservatoREQUIRES_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.
// NESTED: savepoint nella transazione padre
@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 {
// 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:
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 connessioneLa 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.
// 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
}
}// 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.
// 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);
}
}// 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);
}
}Pronto a superare i tuoi colloqui su Spring Boot?
Pratica con i nostri simulatori interattivi, flashcards e test tecnici.
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.
// ERRORE CLASSICO: self-invocation
@Service
public class BrokenService {
private final ItemRepository itemRepository;
public void processItems(List<Long> 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:
// Soluzione 1: Self-injection
@Service
public class FixedServiceWithSelfInjection {
private final ItemRepository itemRepository;
@Lazy
@Autowired
private FixedServiceWithSelfInjection self; // Self-injection
public void processItems(List<Long> 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<Long> 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.
// 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:
// 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.
// 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:
| 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 |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.
// 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<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);
}
}
}Domanda 11: Come testare i diversi comportamenti di propagazione?
I test di propagazione richiedono attenzione particolare per verificare il comportamento transazionale atteso.
// 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");
}
}// 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.
// 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:
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 standardPronto a superare i tuoi colloqui su Spring Boot?
Pratica con i nostri simulatori interattivi, flashcards e test tecnici.
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.
Inizia a praticare!
Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.
Tag
Condividi
Articoli correlati

30 Domande per Colloqui Spring Boot: Guida Completa per Sviluppatori Java
Prepara i tuoi colloqui Spring Boot con queste 30 domande essenziali su auto-configurazione, starter, Spring Data JPA, sicurezza e testing.

Spring Modulith: Architettura del Monolite Modulare Spiegata
Impara Spring Modulith per costruire monoliti modulari in Java. Architettura, moduli, eventi asincroni e testing con esempi Spring Boot 3.

Colloquio Spring Batch 5: Partitioning, Chunk e Fault Tolerance
Padroneggia i colloqui Spring Batch 5: 15 domande essenziali su partitioning, elaborazione a chunk e fault tolerance con esempi in Java 21.