skip to content

TransactionStatus & Savepoints

TransactionStatus tells you whether the transaction is new or already doomed and lets you mark it rollback-only, while the savepoint API supports partial rollback. Knowing setRollbackOnly is often the fix for the silent-rollback bug interviewers describe.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

questions

5

What is TransactionStatus in Spring's programmatic transaction API, and what does setRollbackOnly() do?

level: juniorimportance: must knowfreq 55%

answer

  1. handle returned by getTransaction()
  2. setRollbackOnly = one-way doom switch
  3. commit later -> forced rollback
  4. rollback without throwing
  5. currentTransactionStatus() to reach it declaratively

basics

~10 s

TransactionStatus is a handle to the current transaction that PlatformTransactionManager gives you. Calling setRollbackOnly() marks the transaction so it can only roll back — commit will not succeed.

solid answer

~40 s

When you begin a transaction programmatically via PlatformTransactionManager.getTransaction(...), it returns a TransactionStatus object. This is your control handle for that transaction: you inspect it and steer the outcome. The key method is setRollbackOnly(), which flags the transaction as 'rollback-only' — a one-way switch. Once set, any later commit attempt on that logical transaction is forced to roll back instead. This lets code decide to abort without throwing an exception. You'd call it in a business branch where you detect an invalid state but want to keep control flow. With the higher-level TransactionTemplate you achieve the same by calling status.setRollbackOnly() inside execute(). It complements the declarative @Transactional approach, where an unchecked exception normally triggers rollback automatically.

code

java · 19 lines
java
// Programmatic style
PlatformTransactionManager txManager; // injected

public void transfer(Account from, Account to, BigDecimal amount) {
    TransactionStatus status = txManager.getTransaction(new DefaultTransactionDefinition());
    try {
        from.debit(amount);
        if (from.getBalance().signum() < 0) {
            // decide to abort without throwing
            status.setRollbackOnly();
        } else {
            to.credit(amount);
        }
        txManager.commit(status); // will roll back if rollbackOnly was set
    } catch (RuntimeException ex) {
        txManager.rollback(status);
        throw ex;
    }
}

go deeper

for a junior

Know that TransactionStatus is the handle from getTransaction() and setRollbackOnly() forces a rollback at commit.

for a middle

Know the checked-vs-unchecked rollback default and the currentTransactionStatus() declarative shortcut.

for a senior

Explain why an inner setRollbackOnly leads to UnexpectedRollbackException in the outer commit.

for a principal

Weigh setRollbackOnly vs throwing for control flow, and its interaction with propagation and participating transactions.

## Programmatic transactions Spring offers two styles of transaction management: - *declarative* (`@Transactional` annotations) - and *programmatic* (you call the API yourself). The core interface is `PlatformTransactionManager`, with the single method `TransactionStatus getTransaction(TransactionDefinition definition)`, plus `commit(TransactionStatus)` and `rollback(TransactionStatus)`. ## TransactionStatus When you start a transaction, `getTransaction(...)` hands back a `org.springframework.transaction.TransactionStatus`. Think of it as a *ticket/handle* representing the transaction currently in progress. You pass it back to `commit()` or `rollback()`, and you can query/steer it in the meantime. `TransactionStatus` extends: - `TransactionExecution` (which exposes `isNewTransaction()`, `isRollbackOnly()`, `isCompleted()`) - and `SavepointManager` (savepoint methods, covered separately). ## setRollbackOnly() This method marks the transaction as *rollback-only*. It is a **one-way, sticky flag**: once set it cannot be cleared. The effect is that when someone eventually calls `commit()` on that logical transaction, Spring will **not** commit — it rolls back instead. Use it when your code has decided the unit of work must be abandoned, but you don't want to (or can't) throw an exception to signal that. ## Why not just throw? With declarative `@Transactional`, an escaping *unchecked* exception (a `RuntimeException` or `Error`) triggers automatic rollback; checked exceptions do **not** roll back by default. `setRollbackOnly()` gives you rollback *without* an exception and independent of the checked/unchecked rule — handy when the abort is a normal business branch, or when you catch an exception, log it, and still want the transaction doomed. ## Declarative equivalent Inside an `@Transactional` method you can reach the same flag without holding the status object: - `TransactionInterceptor.currentTransactionStatus().setRollbackOnly();` - or, more commonly, `TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();`. ## Gotcha — silent participation If an *inner* method marks the transaction rollback-only and the *outer* method then tries to commit, Spring throws `UnexpectedRollbackException` at commit time to tell the caller 'you asked to commit but the transaction was already doomed.' That surprises people because the code that set the flag ran without error. ## When to use Prefer declarative `@Transactional` for the common case. Reach for `TransactionStatus.setRollbackOnly()` when you need fine-grained, in-method control: - multi-step programmatic flows, - conditional aborts, - or when integrating with `TransactionTemplate`.

  • Inside a plain @Transactional method, how do you mark the current transaction rollback-only without holding a TransactionStatus reference?
    Call TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() (or TransactionInterceptor.currentTransactionStatus()). It resolves the status bound to the current thread by the transaction interceptor.
  • Does a checked exception thrown from a @Transactional method roll back by default?
    No. By default only unchecked exceptions (RuntimeException/Error) trigger rollback. Checked exceptions commit unless you configure rollbackFor, or you explicitly call setRollbackOnly().

saying these in an interview costs you the question

  • Thinking setRollbackOnly() immediately ends/aborts the transaction (it only sets a flag; rollback happens at commit time)
  • Believing the flag can be reset to false later
  • Assuming all exceptions cause rollback (checked ones don't, by default)

context

open as a page

What does TransactionStatus.isNewTransaction() tell you, and why does it matter with transaction propagation?

level: middleimportance: should knowfreq 40%

basics

~10 s

isNewTransaction() returns true if this call actually started a brand-new physical transaction, and false if it joined an already-running one. It matters because only the outermost, new transaction actually commits or rolls back.

open as a page

What does TransactionStatus.isRollbackOnly() report, and how does it relate to UnexpectedRollbackException?

level: seniorimportance: should knowfreq 35%

basics

~10 s

isRollbackOnly() returns true if the transaction has been marked rollback-only (so it can no longer commit). If an inner method sets that flag and the outer tries to commit, Spring throws UnexpectedRollbackException.

open as a page

How do you use Spring's SavepointManager API (createSavepoint / rollbackToSavepoint / releaseSavepoint) for nested control inside a transaction?

level: seniorimportance: should knowfreq 30%

basics

~10 s

TransactionStatus implements SavepointManager. You call createSavepoint() to mark a point, do risky work, then rollbackToSavepoint(sp) to undo just that work (keeping earlier changes), or releaseSavepoint(sp) to discard the marker if things went fine.

open as a page

In a large batch that must keep good rows and skip bad ones, how would you design transaction handling using savepoints vs REQUIRES_NEW, and what trade-offs drive the choice?

level: principalimportance: nice to knowfreq 18%

basics

~20 s

Wrap each item so a failure only undoes that item. Either use PROPAGATION_NESTED (a savepoint per item inside one big transaction) or PROPAGATION_REQUIRES_NEW (a separate transaction per item). Savepoints share one connection; REQUIRES_NEW commits each item independently.

open as a page