Walk through the correct try/catch pattern for a manager-driven transaction. What goes wrong if the catch block is written incorrectly?
answer
- try work+commit / catch rollback+rethrow
- no auto-rollback rules — you decide
- don't swallow, always rethrow
- catch broadly (RuntimeException)
- commit() itself can throw — don't double-rollback
basics
~10 sGet 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 sThe 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// 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 againgo deeper
Reproduce the try/commit/catch/rollback/rethrow skeleton.
Explain each failure mode and why rollback is not automatic.
Discuss commit() throwing, double-completion, and setRollbackOnly semantics.
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