skip to content

Manager-Driven Transactions

Calling getTransaction, commit and rollback on the PlatformTransactionManager yourself gives full control with no proxy in the way. Interviewers use it to check you understand what @Transactional generates on your behalf.

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

questions

5

Walk through the correct try/catch pattern for a manager-driven transaction. What goes wrong if the catch block is written incorrectly?

level: middleimportance: must knowfreq 45%

answer

  1. try work+commit / catch rollback+rethrow
  2. no auto-rollback rules — you decide
  3. don't swallow, always rethrow
  4. catch broadly (RuntimeException)
  5. commit() itself can throw — don't double-rollback

basics

~10 s

Get a TransactionStatus, do the work, and commit inside try. In catch, call rollback(status) and rethrow. If you forget rollback, a failed transaction stays open and the connection may leak or auto-commit dirty data.

solid answer

~40 s

The pattern is: build a DefaultTransactionDefinition, get a TransactionStatus, then try { work; commit(status); } catch { rollback(status); throw; }. Correctness hinges on the catch: it must cover every exception path that should abort the transaction — normally RuntimeException, but Throwable if you also want to roll back on Error or checked exceptions. Common bugs: (1) forgetting rollback, leaving the transaction open until it times out or the connection is returned dirty; (2) swallowing the exception after rollback, so callers think it succeeded; (3) catching too narrowly (e.g. only a custom checked exception) so a RuntimeException escapes without rollback; (4) calling both commit and rollback on the same status. Note commit() can itself throw (e.g. constraint check on flush), so a robust version may guard rollback and be careful not to double-complete the transaction.

code

java · 11 lines
java
// Defensive variant: commit outside the catch so a commit failure
// (Spring already rolled back internally) isn't rolled back twice.
TransactionStatus status = txManager.getTransaction(def);
try {
    orderDao.insert(order);
    inventoryDao.decrement(order.items());
} catch (RuntimeException ex) {
    txManager.rollback(status);
    throw ex;
}
txManager.commit(status); // may throw TransactionSystemException; do NOT rollback again

go deeper

for a junior

Reproduce the try/commit/catch/rollback/rethrow skeleton.

for a middle

Explain each failure mode and why rollback is not automatic.

for a senior

Discuss commit() throwing, double-completion, and setRollbackOnly semantics.

for a principal

Weigh this boilerplate against TransactionTemplate and justify when hand-rolling is warranted.

## The canonical shape ```java TransactionStatus status = txManager.getTransaction(def); try { doWork(); txManager.commit(status); } catch (RuntimeException ex) { txManager.rollback(status); throw ex; } ``` Every element matters, so let's dissect the failure modes. ## Failure mode 1 — forgetting rollback Unlike `@Transactional`, the manager applies **no automatic rollback rules**. If an exception escapes the `try` and you never call `rollback(status)`, the transaction is neither committed nor rolled back by Spring. Depending on the manager and connection pool, the physical connection may be returned to the pool with an open transaction, get rolled back later on close, or in a misconfigured setup even auto-commit partial work. At minimum you risk holding locks and connections until a timeout. ## Failure mode 2 — swallowing the exception If you `rollback(status)` but then **do not rethrow**, the caller sees a normal return and assumes the work committed. Almost always you should rethrow (or throw a translated exception) after rollback so the failure is visible. ## Failure mode 3 — catch too narrow Catching only, say, `BusinessException` means a `RuntimeException` (NPE, `DataAccessException`, `OptimisticLockingFailureException`) propagates past the `catch` with **no rollback**. Because you own the rollback decision, the catch must be broad enough. `catch (RuntimeException ex)` covers Spring's `DataAccessException` hierarchy (all unchecked). Use `catch (Throwable t)` only if you deliberately want to roll back on `Error` and checked exceptions too — and rethrow appropriately. ## Failure mode 4 — commit() throws `txManager.commit(status)` can throw — e.g. deferred constraint violation surfaced at flush/commit, or `TransactionSystemException`. Importantly, when `commit()` fails, Spring already rolls the transaction back internally, so you must **not** call `rollback(status)` again on that same status (it's already completed). A defensive pattern separates the two: ```java TransactionStatus status = txManager.getTransaction(def); try { doWork(); } catch (RuntimeException ex) { txManager.rollback(status); // work failed before commit throw ex; } txManager.commit(status); // may throw; already rolled back internally on failure ``` Here rollback covers only the work phase; the commit is outside the catch so a commit failure isn't double-handled. ## setRollbackOnly as an alternative signal Instead of exceptions, you can mark `status.setRollbackOnly()` from inner logic; the eventual `commit(status)` then performs a rollback (and Spring may throw `UnexpectedRollbackException` to signal the silent rollback). This mirrors how nested `@Transactional` participation flags a rollback. ## Why this is error-prone All four failure modes are exactly why Spring recommends `TransactionTemplate` for single blocks: it runs your callback, commits on normal return, and automatically rolls back on `RuntimeException`/`Error` (or when you call `status.setRollbackOnly()`), removing the hand-written catch. Direct manager use is warranted only when you need boundaries the template can't express.

  • Why should the catch clause usually be RuntimeException rather than a specific business exception?
    Because you own the rollback decision. A too-narrow catch lets other unchecked exceptions (NPE, Spring's DataAccessException, optimistic-lock failures) escape without rolling back. RuntimeException covers Spring's unchecked DataAccessException hierarchy.
  • What does status.setRollbackOnly() do compared to calling rollback()?
    setRollbackOnly marks the transaction so the eventual commit(status) actually performs a rollback instead of committing. It defers the decision; Spring may raise UnexpectedRollbackException to signal the commit was silently turned into a rollback.

saying these in an interview costs you the question

  • Assuming the manager auto-rolls-back on exceptions like @Transactional does
  • Calling rollback(status) after commit(status) already failed
  • Swallowing the exception after rollback so the caller sees success
  • Catching only a checked business exception and letting RuntimeExceptions escape

context

open as a page

How does rollback behavior with the direct PlatformTransactionManager differ from @Transactional's default rule-based rollback?

level: seniorimportance: must knowfreq 50%

basics

~20 s

@Transactional rolls back automatically only on unchecked exceptions (RuntimeException/Error) and commits on checked ones. With the direct manager there are no rules at all — you must call rollback() yourself for whatever cases you want to abort.

open as a page

What is manager-driven programmatic transaction management, and how do you use PlatformTransactionManager directly?

level: juniorimportance: should knowfreq 35%

basics

~10 s

Instead of the @Transactional annotation, you inject PlatformTransactionManager, call getTransaction(...) to start a transaction, run your code, then call commit() on success or rollback() on failure yourself.

open as a page

Why does manager-driven transaction management avoid the proxy self-invocation problem, and what are the trade-offs versus @Transactional?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Because there is no AOP proxy involved — the transaction starts because your code calls getTransaction() directly, not because a proxy intercepted a call. So @Transactional's self-invocation gotcha (private/internal calls bypassing the proxy) simply doesn't exist. The cost is more boilerplate.

open as a page

When would you choose the raw PlatformTransactionManager over TransactionTemplate or @Transactional, and how do savepoints and per-unit commits fit in?

level: principalimportance: nice to knowfreq 20%

basics

~20 s

Use the raw manager only when the higher abstractions can't express the boundary: many independent commits in a loop, boundaries opened in one place and closed in another, or savepoints for partial rollback. Otherwise prefer @Transactional, then TransactionTemplate.

open as a page