You need per-item partial rollback in a large batch. Discuss the trade-offs of Propagation.NESTED (savepoints) versus alternatives at scale, including locking and failure semantics.
answer
- savepoint keeps pre-savepoint locks until final commit
- one long tx = lock + undo/WAL growth
- chunked commit = durable progress, no global atomicity
- REQUIRES_NEW loop = connection/pool exhaustion
- idempotent + retry + outbox = scale winner
basics
~20 sNESTED gives cheap per-item rollback via savepoints within one transaction, but the whole batch is still one long transaction holding locks and one connection. Alternatives — chunked commits, REQUIRES_NEW per item, or idempotent retries — trade atomicity for shorter locks and independent durability.
solid answer
~50 sNESTED lets you wrap each item in a savepoint so a single failure rolls back just that item while the batch continues, all within one physical transaction and one connection — efficient and simple. The cost is that it remains one long-lived transaction: locks acquired before a savepoint are held until the final commit, the undo/redo log grows, and everything commits or aborts as a unit, so a late fatal error discards the whole batch. Alternatives: chunked commits (commit every N items) cap lock duration and transaction size but lose all-or-nothing; REQUIRES_NEW per item makes each item independently durable but consumes a connection per active inner transaction and risks pool exhaustion; idempotent per-item processing with retries and an outbox/dead-letter queue scales best but needs careful design. Choose NESTED when the batch genuinely must be atomic overall yet tolerate skipping bad items, and the batch is bounded so lock/log growth is acceptable.
code
java · 18 lines// NESTED: atomic-overall batch that skips bad items,
// but ONE long transaction (watch lock/log growth on large N)
@Transactional(transactionManager = "jdbcTxManager")
public BatchResult process(List<Item> items) {
int skipped = 0;
for (Item it : items) {
try {
itemService.handle(it); // @Transactional(propagation = NESTED)
} catch (BusinessException ex) {
skipped++; // rolled back to this item's savepoint
}
}
return new BatchResult(items.size() - skipped, skipped);
// A fatal error here still discards the WHOLE batch — that's the trade-off.
}
// Contrast — chunked commit caps lock duration but loses global atomicity:
// for (var chunk : partition(items, 500)) { txTemplate.execute(s -> { ... }); }go deeper
Not expected to reason about this depth; can state NESTED = per-item savepoint.
Should recognize NESTED is still one long transaction and mention chunking as an alternative.
Should articulate lock-retention semantics and contrast NESTED/REQUIRES_NEW/chunking with connection and durability implications.
Should drive the full trade-off analysis — locking, undo/WAL growth, pool exhaustion, atomicity vs. incremental durability — and pick per context, invoking Spring Batch/outbox where appropriate.
## The scenario Process N items; a bad item should be undone and skipped, but you want control over how the batch as a whole succeeds or fails. Several patterns exist; NESTED is one point in the trade-off space. ## Option A — NESTED savepoints (one transaction, per-item savepoint) **How**: outer `@Transactional`; each item processed via a `Propagation.NESTED` method so a savepoint is set per item and only the failing item rolls back. **Locking reality**: a savepoint does **not** release locks acquired *before* it. Rolling back to a savepoint releases locks taken *after* it (the failed item's), but every row/index lock the transaction accumulated across successful items is held until the **final commit**. For a large batch this is a long-lived, wide-locking transaction — a source of contention, lock escalation, and blocking. **Log/undo growth**: the DB's undo/rollback segment and redo/WAL keep growing for the whole transaction lifetime; very large batches can pressure the DB (e.g., Postgres bloat, Oracle ORA-01555 snapshot-too-old for concurrent readers, MySQL undo growth). **Failure semantics**: still atomic overall — a fatal error near the end (or an explicit outer rollback) discards **all** items, even the ones whose savepoints succeeded. Good if you want all-or-nothing-with-skips; bad if you want committed progress. **Connection**: one connection for the whole batch — low pool pressure. Requires a savepoint-capable manager (`DataSourceTransactionManager`). ## Option B — Chunked commits (Spring Batch style) Commit every N items in their own transaction. Caps lock duration and transaction/log size, gives incremental durable progress, enables restart-from-last-chunk. Cost: **loses global atomicity** — a later chunk failing doesn't undo earlier committed chunks; you need restartability/compensation. This is what Spring Batch's chunk-oriented processing does and is usually the right answer at true scale. ## Option C — REQUIRES_NEW per item Each item commits independently. Durable progress per item and clean isolation of failures. Cost: **a connection held per active inner transaction** (outer suspended + inner active), so looping REQUIRES_NEW over a large batch can **exhaust the pool / self-deadlock**; also more commit overhead. Fine for a handful of independent side effects, poor as a per-row loop over thousands. ## Option D — Idempotent processing + retry + DLQ Make each item's processing idempotent (natural keys, upserts), process independently, retry transient failures, route permanent failures to a dead-letter queue; use the **outbox pattern** for downstream effects. Scales horizontally, shortest locks, no giant transaction. Cost: most design effort and eventual-consistency reasoning. ## Choosing - Small/bounded batch that must be atomic-overall but tolerate skipping bad rows, on a JDBC manager → **NESTED**. - Large batch, want durable incremental progress and restart → **chunked commits / Spring Batch**. - A few independent must-persist side effects → **REQUIRES_NEW** (sparingly). - High throughput, distributed, resilience-first → **idempotent + retry + outbox**. ## Cross-cutting gotchas - **Manager support**: NESTED needs `DataSourceTransactionManager`/`JdbcTransactionManager`; fails on `JpaTransactionManager` by default. - **ORM state**: savepoint rollback reverts DB rows but not Hibernate's first-level cache — mixing NESTED with JPA risks stale entity state. - **Proxy self-invocation**: per-item NESTED/REQUIRES_NEW calls must cross a Spring proxy or propagation is ignored. - **Rollback rules**: only rollback-triggering exceptions actually trip the savepoint rollback; a caught exception leaves the savepoint dangling (released at method end). - **Isolation & concurrency**: one long NESTED transaction can block other writers on the locked rows for its entire duration — measure contention, not just correctness.
- Does rolling back to a savepoint release the locks held by earlier successful items in the same transaction?No. Rollback-to-savepoint releases only locks acquired after that savepoint (the failed item's). Locks taken before it, across earlier successful items, are retained until the transaction's final commit or a full rollback.
- For a 1-million-row import, why is a single NESTED transaction usually the wrong tool?It's one long-lived transaction holding accumulating locks and growing undo/redo the whole time, hurting concurrency and risking snapshot-too-old / bloat, and a late failure discards everything. Chunked commits or idempotent per-item processing scale far better.
saying these in an interview costs you the question
- Claiming a savepoint rollback frees all locks taken so far
- Using a REQUIRES_NEW loop over thousands of rows without considering pool exhaustion
- Assuming NESTED gives durable per-item progress (it doesn't — outer rollback wipes all)
- Treating NESTED as a scalability tool rather than a bounded-batch atomicity tool