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.

Spring Boot Propagazione delle Transazioni: domande di colloquio ed esempi pratici

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?»

OrderService.javajava
// 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.

UserService.javajava
// 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.

PaymentService.javajava
// 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.

BatchProcessingService.javajava
// 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:

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.

ReportingService.javajava
// 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
    }
}
OrderService.javajava
// 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.

AuditService.javajava
// 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);
    }
}
SecurityService.javajava
// 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.

BrokenService.javajava
// 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:

java
// 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.

TransactionRollbackDemo.javajava
// 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:

BaseTransactionalService.javajava
// 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.

IsolationDemo.javajava
// 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.

OutboxService.javajava
// 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.

TransactionPropagationTest.javajava
// 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");
    }
}
PropagationIntegrationTest.javajava
// 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.

TransactionConfig.javajava
// 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

Pronto 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

#spring boot
#transactions
#propagation
#java
#interview

Condividi

Articoli correlati