skip to content

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

level: seniorimportance: must knowfreq 50%

answer

  1. @Transactional: unchecked→rollback, checked→commit (tunable)
  2. direct manager: no rules, you call rollback
  3. checked exception commits under @Transactional — surprise
  4. handled-in-method → proxy commits; setRollbackOnly to override
  5. per-row commit needs manager/REQUIRES_NEW

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.

solid answer

~40 s

@Transactional is driven by an AOP proxy that applies rollback rules: by default it rolls back on RuntimeException and Error and commits on checked exceptions, tunable via rollbackFor/noRollbackFor. The direct manager has no proxy and no rules — getTransaction/commit/rollback do exactly and only what you call. So checked-vs-unchecked distinction is irrelevant; your catch block decides. This is the 'full control' selling point: you can commit even when an exception was thrown-and-handled, roll back on a checked exception (which @Transactional would commit), or commit some units and roll back others in a loop. The trade-off is you lose Spring's convenient defaults and must implement correct rollback logic by hand. TransactionTemplate sits in between: it re-applies the unchecked-rollback rule automatically around a callback.

code

java · 10 lines
java
// Business case @Transactional's defaults get wrong: roll back on a CHECKED exception.
// @Transactional would COMMIT here unless you added rollbackFor. The manager makes it explicit.
TransactionStatus status = txManager.getTransaction(def);
try {
    ledger.post(entry);          // may throw checked ReconciliationException
    txManager.commit(status);
} catch (Exception ex) {          // note: checked Exception, not just RuntimeException
    txManager.rollback(status);   // we WANT rollback on the checked failure
    throw ex;
}

go deeper

for a junior

Just know @Transactional auto-rolls-back and the manager doesn't.

for a middle

State the unchecked→rollback / checked→commit default and that the manager has no rules.

for a senior

Name the mechanism (TransactionInterceptor/rollbackOn) and give a concrete case justifying the manager.

for a principal

Frame the choice as a control-vs-boilerplate trade-off and cover rollback-only propagation across boundaries.

## Two different mechanisms **`@Transactional`** works through an AOP proxy (`TransactionInterceptor`). The proxy opens a transaction before the method, and after the method decides commit vs rollback using **rollback rules**. Defaults, implemented in `DefaultTransactionAttribute.rollbackOn(...)`: - Roll back on `RuntimeException` (unchecked) and `Error`. - **Commit** on checked `Exception`. You override with `@Transactional(rollbackFor = SomeCheckedException.class)` or `noRollbackFor = ...`. **Direct `PlatformTransactionManager`** has no proxy and no rules. `commit(status)` commits, `rollback(status)` rolls back — full stop. Whether an exception was checked or unchecked means nothing; only your code decides. ## The practical consequences ### 1. Checked exceptions With `@Transactional`, throwing a checked `IOException` from the method **commits** the transaction by default — a classic surprise. With the direct manager, you write `catch (Exception ex) { rollback(status); throw ex; }` and the checked exception rolls back because you said so. You have explicit, local control. ### 2. Handled exceptions With `@Transactional`, if an exception is thrown *inside* the method but you catch and handle it so nothing propagates out of the proxied method, the transaction **commits** (the proxy never saw an exception). Conversely if you catch it but still want a rollback you must call `TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()`. With the direct manager there's no such indirection — you literally choose commit or rollback at the point of handling. ### 3. Per-iteration control In a loop that should commit good rows and skip bad ones, the direct manager (or a `REQUIRES_NEW` template per row) lets you commit and roll back independently per iteration — something a single method-level `@Transactional` can't express (one exception rolls back the whole method's transaction, and a marked-rollback-only transaction 'poisons' any outer participation). ### 4. Rollback-only propagation Under `@Transactional`, an inner `PROPAGATION_REQUIRED` transaction that fails marks the *shared* transaction rollback-only; the outer commit then throws `UnexpectedRollbackException`. The direct manager exposes the same machinery via `status.setRollbackOnly()` / `status.isRollbackOnly()`, but you invoke it explicitly. ## Summary table (conceptual) - `@Transactional`: proxy + automatic rule-based rollback (unchecked → rollback, checked → commit, tunable). Least code, least control. - `TransactionTemplate`: callback; auto rollback on `RuntimeException`/`Error` or `setRollbackOnly()`, commit otherwise. Medium. - Direct `PlatformTransactionManager`: no rules; you call commit/rollback. Most code, most control, used when boundaries or commit logic can't be expressed declaratively. ## Interview-grade nuance A strong answer names `TransactionInterceptor`/`DefaultTransactionAttribute.rollbackOn` as *where* the rule lives, states that the direct manager bypasses all of it, and gives a concrete case (commit-on-handled-checked-exception, or per-row commit in a batch) that justifies dropping to the manager rather than tuning `rollbackFor`.

  • Under @Transactional, a method catches a RuntimeException internally and returns normally. Does the transaction commit or roll back?
    It commits. The proxy only sees what escapes the method, so a swallowed exception looks like success. To force rollback you must call TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(). The direct manager avoids this indirection — you choose commit or rollback at the catch.
  • Where does @Transactional's default rollback rule actually live?
    In DefaultTransactionAttribute.rollbackOn(...), invoked by TransactionInterceptor / TransactionAspectSupport after the proxied method. The direct PlatformTransactionManager bypasses this class entirely.

saying these in an interview costs you the question

  • Claiming @Transactional rolls back on checked exceptions by default (it commits)
  • Believing the direct manager applies the same unchecked-vs-checked rule (it applies none)
  • Thinking a swallowed exception under @Transactional still triggers rollback

context