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?
answer
- NESTED = one tx, one connection, deferred durability
- REQUIRES_NEW = per-item commit, survives later failures, connection cost
- chunk-commit (Spring Batch) is the real-world answer
- idempotency + flush/clear + pool sizing
- shared REQUIRED + try/catch => UnexpectedRollbackException
basics
~20 sWrap 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.
solid answer
~50 sBoth approaches isolate per-item failure, but they differ in durability, resource use, and atomicity. With PROPAGATION_NESTED (savepoints via SavepointManager), all items live in one physical transaction on one connection: a bad item is undone with rollbackToSavepoint while good items stay, but nothing is durable until the single outer commit — and if that final commit fails, the whole batch is lost. With PROPAGATION_REQUIRES_NEW, each item is its own physical transaction that commits immediately and independently, so completed items survive even if a later item or the driver fails; the cost is a separate connection per item (risking pool exhaustion) and no all-or-nothing envelope. For huge batches I'd usually chunk (commit every N items, Spring Batch style) to bound transaction size and lock duration, choose NESTED when I want one atomic envelope with cheap per-item undo, and REQUIRES_NEW when partial durability and progress preservation matter most. I'd also confirm the transaction manager/driver supports savepoints, watch ORM flush timing, and make item processing idempotent for safe retries.
code
java · 22 lines// Option A: savepoints within ONE transaction (atomic envelope, cheap undo)
void runNested(List<Item> items) {
TransactionStatus tx = txManager.getTransaction(new DefaultTransactionDefinition());
for (Item item : items) {
Object sp = tx.createSavepoint();
try {
process(item);
tx.releaseSavepoint(sp);
} catch (RuntimeException ex) {
tx.rollbackToSavepoint(sp); // undo just this item; batch stays open
skipLog.add(item.id(), ex);
}
}
txManager.commit(tx); // if THIS fails, entire batch is lost
}
// Option B: independent transaction per item (durable progress, more connections)
@Service
class ItemProcessor {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void processOne(Item item) { /* commits on its own */ }
}go deeper
Understand the goal: isolate a bad row so the batch continues.
Know NESTED vs REQUIRES_NEW isolate failures differently and that a shared REQUIRED try/catch fails.
Compare durability, connection cost, and transaction footprint; know savepoint support constraints.
Choose among NESTED/REQUIRES_NEW/chunking with explicit trade-offs on durability, resource use, atomicity, idempotency, and ORM/flush behavior.
**The problem.** You process thousands of rows; some will fail validation or hit a constraint. You want to *keep the good ones* and *skip the bad ones* without aborting the whole run — while keeping the database consistent and performance sane. **Option A — PROPAGATION_NESTED (savepoints).** Open one outer physical transaction. For each item, Spring (or you, via `TransactionStatus.createSavepoint()`) sets a savepoint; on failure it does `rollbackToSavepoint(handle)` to undo just that item and continues; on success it releases the savepoint. Characteristics: - **One connection, one physical transaction.** Low connection churn. - **Deferred durability.** Nothing is committed until the single outer `commit()`. If that final commit fails (deadlock, connection drop), *all* processed items are lost. - **Cheap per-item undo** via savepoints. - **Large transaction footprint.** Locks/undo-log/temp state accumulate for the whole batch — long-running transaction, more lock contention, bigger rollback segment (e.g., Postgres bloat, Oracle UNDO pressure). - **Requires savepoint support** (`DataSourceTransactionManager`/`JpaTransactionManager` + capable driver; not plain JTA). Otherwise `NestedTransactionNotSupportedException`. **Option B — PROPAGATION_REQUIRES_NEW.** Each item runs in its *own* physical transaction that commits independently (the outer scope, if any, is suspended around each). Characteristics: - **Immediate, independent durability.** Completed items survive even if a later item, the driver, or the process fails — great for progress preservation and resumability. - **No atomic envelope.** There's no single commit that makes the batch all-or-nothing; you accept partial completion by design. - **Connection cost.** Each REQUIRES_NEW suspends the current transaction and acquires *another* connection; deep or concurrent nesting can exhaust the pool. Mitigate with adequate pool sizing and by not holding an outer transaction open around the loop. - **Short transactions.** Small lock/undo footprint per item — friendlier under contention. **Option C — Chunking (the usual production answer).** Neither per-item extreme scales best. The Spring Batch pattern commits every *N* items (a chunk) in one transaction, with skip/retry policies. This bounds transaction size and lock duration, amortizes commit cost, and gives tunable durability granularity. On a chunk failure you can retry the chunk or fall back to item-level processing to isolate the poison row. This blends the two: atomic per-chunk, durable across chunks. **Cross-cutting concerns:** - **Idempotency & retries.** Because items may be retried (chunk retry, or resume after crash), item processing should be idempotent (e.g., upsert semantics, dedupe keys) to avoid double effects. - **ORM flush timing.** With JPA/Hibernate, unflushed changes and savepoint/commit boundaries interact — you may need explicit `flush()` so a savepoint captures intended state, and to surface constraint violations at the right point. Clear the persistence context between chunks to bound memory and avoid stale first-level cache. - **Rollback-only propagation.** If items run under one shared REQUIRED transaction and one throws, the whole transaction becomes rollback-only → `UnexpectedRollbackException`. That's exactly why you isolate with NESTED or REQUIRES_NEW; naive try/catch in a shared transaction does not work. - **Observability.** Record skipped items (id + reason) — but note that under Option A the 'skip log' rows are also in the one transaction and vanish if the final commit fails; under Option B/chunking they persist. - **Manager support & portability.** Confirm savepoint support if choosing NESTED; verify isolation-level and lock behavior of your DB; consider deadlock retry. **Decision guide.** Want one atomic envelope with cheap per-item undo and the batch is bounded → NESTED. Want progress preserved / resumable / partial success acceptable → REQUIRES_NEW. Large-scale, production ETL → chunked (Spring Batch) with skip/retry, item-level fallback for poison rows. Always: idempotent items, sane pool sizing, explicit flush/clear with ORM, and deadlock/retry handling.
- Why can REQUIRES_NEW-per-item exhaust the connection pool where NESTED does not?REQUIRES_NEW suspends the current transaction and acquires a separate connection for each new transaction; nesting or holding an outer transaction open around the loop means multiple connections are held at once. NESTED reuses the single connection of the one physical transaction.
- Why is per-item REQUIRES_NEW usually replaced by chunk-commit in real batch systems?Committing every single item has high commit overhead and connection churn; chunking commits every N items in one transaction, bounding transaction size and lock duration while amortizing commit cost, with skip/retry policies for poison rows.
- With Option A, where do your 'skipped item' audit rows end up if the final commit fails?They're part of the same single transaction, so they're rolled back and lost along with everything else — a reason to prefer REQUIRES_NEW or a separate audit transaction for durable skip logging.
saying these in an interview costs you the question
- Claiming savepoint (NESTED) changes are durable before the outer commit
- Using a shared REQUIRED transaction with try/catch and expecting per-item isolation (hits UnexpectedRollbackException)
- Ignoring connection-pool exhaustion with per-item REQUIRES_NEW
- Forgetting ORM flush/clear and idempotency in retryable batches