When would you choose the raw PlatformTransactionManager over TransactionTemplate or @Transactional, and how do savepoints and per-unit commits fit in?
answer
- ladder: @Transactional → TransactionTemplate → raw manager
- per-item / chunk commit in a loop
- begin here, commit there (cross-method boundary)
- createSavepoint / rollbackToSavepoint = PROPAGATION_NESTED
- contain it: one tested helper, name the definition
basics
~20 sUse 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.
solid answer
~40 sNinety percent of code should use @Transactional; a scoped block should use TransactionTemplate. The raw PlatformTransactionManager is the escape hatch for boundaries neither can express cleanly: (1) commit-per-item batch loops where each iteration is its own transaction and one bad row shouldn't roll back the good ones; (2) transactions whose begin and commit live in different methods/callbacks (e.g. streaming, chunk processing, framework code); (3) partial rollback via savepoints — status.createSavepoint()/rollbackToSavepoint(), which maps to PROPAGATION_NESTED and requires JDBC savepoint support; (4) infrastructure where no proxy/AOP exists. The costs are boilerplate, scattered transaction concerns, and manual propagation reasoning. As a principal I'd wrap any repeated raw usage in a small helper and document why the abstraction didn't fit, so the escape hatch doesn't leak everywhere.
code
java · 16 lines// Commit-per-chunk batch: one bad chunk doesn't roll back the good ones.
public void importAll(List<Row> rows) {
for (List<Row> chunk : partition(rows, 500)) {
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setName("import-chunk");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionStatus status = txManager.getTransaction(def);
try {
chunk.forEach(dao::insert);
txManager.commit(status); // this chunk is durably saved
} catch (RuntimeException ex) {
txManager.rollback(status); // only this chunk is discarded
log.warn("Chunk failed, continuing", ex);
}
}
}go deeper
Know that @Transactional is the normal choice and the manager is rare.
Name a case or two (batch loops) where you'd go programmatic.
Explain savepoints/NESTED and REQUIRES_NEW-per-item, plus the boilerplate cost.
Frame the decision ladder, governance (contain/encapsulate), and observability/testability of hand-rolled boundaries.
## The decision ladder 1. **`@Transactional`** — default. Declarative, clean, composes via propagation. Use unless you hit its limits (self-invocation, per-item boundaries, dynamic boundaries). 2. **`TransactionTemplate`** — a scoped programmatic block with automatic rollback-on-`RuntimeException` and `execute(...)`/`executeWithoutResult(...)` callbacks. Use when you want a transaction around a specific block without a proxy, but still want the auto-rollback convenience. 3. **Raw `PlatformTransactionManager`** — the lowest level. Choose it only when even the template is too rigid. ## Cases that justify the raw manager ### 1. Commit-per-item batch processing Processing 100k rows in one transaction is a bad idea (huge undo log, long locks, all-or-nothing). You want each row (or each chunk of N) in its own transaction so failures are isolated and progress is durable. You can do this by looping and calling `getTransaction`/`commit`/`rollback` per chunk (or a `TransactionTemplate` with `PROPAGATION_REQUIRES_NEW` per item). Spring Batch does exactly this kind of chunk-oriented commit under the hood. ### 2. Boundaries that span methods/callbacks Sometimes you must **begin** a transaction in one place and **commit** it in another — e.g. an event-driven flow, a resource that opens a unit of work and closes it on a later callback, or a manual streaming reader/writer. `@Transactional` can't express a boundary that isn't a method scope; the raw manager holds the `TransactionStatus` and lets you commit later. ### 3. Savepoints / partial rollback `TransactionStatus` (via `SavepointManager`) supports: ```java Object savepoint = status.createSavepoint(); try { riskyOptionalStep(); } catch (RuntimeException ex) { status.rollbackToSavepoint(savepoint); // undo just this step, keep the rest } // ... continue and eventually commit the whole transaction ... status.releaseSavepoint(savepoint); ``` This is the programmatic face of `PROPAGATION_NESTED` and requires a manager/driver supporting JDBC 3.0 savepoints (`DataSourceTransactionManager` does; classic `JtaTransactionManager` does not). It lets an optional sub-step fail without discarding the outer work — impossible to express with a single `@Transactional`. ### 4. Conditional / dynamic commit When whether-and-when to commit depends on runtime state computed mid-flow, explicit `commit`/`rollback`/`setRollbackOnly` calls read more clearly than contorting annotations. ### 5. Infrastructure / no-AOP contexts Library or bootstrap code that runs before/outside the AOP-managed bean graph can still get transactions via the injected manager. ## Costs and governance (the principal lens) - **Boilerplate & tangling:** transaction management leaks into business logic; the declarative model existed to remove exactly this. Contain it. - **Manual propagation:** you must reason about `PROPAGATION_REQUIRED`/`REQUIRES_NEW`/`NESTED` yourself; nested participation and `UnexpectedRollbackException` semantics still apply. - **Consistency risk:** hand-written try/catch is easy to get subtly wrong (double-complete, swallowed exceptions). Prefer `TransactionTemplate` whenever it suffices; reserve the raw manager for the genuine outliers. - **Testability & observability:** name your definitions (`def.setName(...)`) so transactions are identifiable in monitoring; wrap recurring patterns in a small internal utility with tests. ## What a strong principal answer sounds like 'Default `@Transactional`; `TransactionTemplate` for scoped blocks; raw manager only for per-item commit loops, cross-method boundaries, or savepoint-based partial rollback — and when I do reach for it, I encapsulate the try/catch/commit/rollback in one tested helper, name the definition for observability, and document why the declarative model didn't fit, so the escape hatch doesn't proliferate.'
- How do savepoints via TransactionStatus relate to propagation behavior?status.createSavepoint()/rollbackToSavepoint() are the programmatic form of PROPAGATION_NESTED: a nested unit inside one physical transaction that can be partially rolled back to a savepoint while the outer work survives. It needs a manager/driver supporting JDBC savepoints (DataSourceTransactionManager does; classic JTA does not).
- If you need per-item isolation but want less boilerplate, what's a good middle ground?A TransactionTemplate configured with PROPAGATION_REQUIRES_NEW, called once per item/chunk inside the loop. It gives independent commits with automatic rollback-on-RuntimeException, avoiding hand-written try/catch while still isolating failures.
saying these in an interview costs you the question
- Recommending the raw manager as a general default over @Transactional
- Claiming savepoints work with any transaction manager (classic JTA doesn't support them)
- Thinking a single @Transactional method can commit some rows and roll back others