AI Briefing
KO

A Deep Dive into Spring Transactional Rollback

·2025.02.10 09:00

Key point

Proxies, rollback marking, propagation options, and Kotlin exception conversion determine what actually gets rolled back.

1 / 2

Details

In Spring transactions, Unchecked Exception is by default the criterion for rollback marking. However, catching an exception doesn't always mean it's safe—what actually matters is whether TransactionInterceptor got involved, and to which transaction and at what point the exception was delivered.

When you directly call a @Transactional method within the same service, it doesn't go through the proxy, so a new transaction is not opened. This means that even if you attach REQUIRES_NEW, the expected separation behavior won't occur for internal calls; to actually separate the transaction, you need to split it into a separate service or use a transaction template.

The relationship between exception handling and rollback is also not simple. If a @Transactional method in another service is called through a proxy and throws a RuntimeException, rollback gets marked—but even if the calling method catches the exception, if it's already marked, it will eventually be rolled back at commit time. Conversely, if an exception occurs in a method that didn't go through the proxy and the caller handles it, no rollback marking remains at all, so it can commit normally.

Checked Exception is not a rollback target by default, but in Kotlin, exception handling behaves differently from Java, which can produce unexpected results. When a Checked Exception like IOException crosses a Java interface boundary and gets converted into an UndeclaredThrowableException, it gets treated as part of the RuntimeException family and can trigger a rollback. To prevent this, you need to explicitly declare @Throws(IOException::class) on the Kotlin function so the exception is delivered in its intended form.

@Transactional(propagation = Propagation.REQUIRES_NEW) runs on the same thread but creates a new transaction. So a failure inside the new transaction only leaves rollback marking on that transaction, and if the parent transaction handles the exception, the parent transaction can continue to commit. Conversely, even if the parent transaction fails later, the already-completed child transaction won't be rolled back again.

As a real-world example, an approach is presented where work is separated in a Kafka listener to avoid holding a long-running transaction. The key points are as follows.

  • The listener retries with nack on failure and calls acknowledge only on success.
  • Large tasks like deletePersonalInfo are managed at the parent transaction level, but internal deletion work is kept short by cutting it off with REQUIRES_NEW.
  • If there's too much data, it's further split into batches or cursor-based chunks and processed as multiple short transactions.

Ultimately, what matters is the type of exception, whether it went through the proxy, which transaction the rollback marking landed on, and the combination of thread and propagation options. Understanding these four points precisely is essential for designing Spring @Transactional reliably.

This summary was generated automatically by AI. Check the original for the author's claims and context. Copyright belongs to the original author.

Our guide explains how the AI works. Report summary errors, attribution issues, or removal requests via Contact.