What Does Propagation NESTED Do
If an active transaction already exists:
🟢 Junior Level
Propagation.NESTED is a transaction propagation type in Spring Framework that executes within a nested logical transaction backed by a JDBC Savepoint inside an already existing physical database transaction.
The Core Concept in 30 Seconds
If an active transaction already exists:
- Spring does not open a second database connection (unlike
REQUIRES_NEW); instead, it creates a JDBC Savepoint on the current connection. - If an exception occurs inside the nested method, the database rolls back only to that specific savepoint (
ROLLBACK TO SAVEPOINT). - The outer (parent) transaction can catch the exception in a
try-catchblock, continue execution, and successfully commit the rest of its work. - However, if the outer (parent) transaction ultimately fails or rolls back, all changes are completely rolled back, including those made by successfully executed nested methods.
If no transaction exists when entering a NESTED method, it behaves identically to REQUIRED (opens a new physical transaction).
Execution Timeline
Outer Transaction (REQUIRED)
├── START TRANSACTION (Connection #1)
│ INSERT INTO orders ... (Success)
│
│ Nested Call (NESTED):
│ ├── SAVEPOINT nested_sp_1
│ │ INSERT INTO order_items ... (FAILS WITH EXCEPTION!)
│ └── ROLLBACK TO SAVEPOINT nested_sp_1 <-- Only order_items rolled back!
│
│ Outer method catches error in try-catch:
│ INSERT INTO audit_log ... (Success)
└── COMMIT (Saves orders and audit_log; order_items is cleanly omitted!)
Practical Example: Batch Processing with Partial Failure Recovery
@Slf4j
@Service
public class BatchOrderProcessor {
private final SingleOrderService singleOrderService;
public BatchOrderProcessor(SingleOrderService singleOrderService) {
this.singleOrderService = singleOrderService;
}
@Transactional // Outer transaction: Single physical JDBC connection
public void processBatch(List<OrderDto> orders) {
for (OrderDto order : orders) {
try {
// Each order executes inside its own nested JDBC Savepoint
singleOrderService.processSingleOrder(order);
} catch (Exception ex) {
// Only this specific order rolls back; the overall batch continues!
log.error("Failed to process order #{}: {}", order.getId(), ex.getMessage());
}
}
// Final COMMIT persists all successfully processed orders
}
}
@Service
public class SingleOrderService {
private final OrderJdbcRepository orderJdbcRepository;
public SingleOrderService(OrderJdbcRepository orderJdbcRepository) {
this.orderJdbcRepository = orderJdbcRepository;
}
@Transactional(propagation = Propagation.NESTED)
public void processSingleOrder(OrderDto order) {
orderJdbcRepository.save(order);
if (order.getAmount() <= 0) {
// Throwing an exception rolls back strictly to this order's SAVEPOINT
throw new IllegalArgumentException("Order amount must be positive");
}
}
}
🟡 Middle Level
Architectural Pipeline: How Spring Implements NESTED via Savepoints
Under the hood, Propagation.NESTED relies on the SavepointManager abstraction (implemented by JdbcTransactionObjectSupport):
Caller: batchService.process() ──► singleService.processSingleOrder()
│
▼
[ Spring AOP Proxy ]
│
▼
[ TransactionInterceptor ]
│
1. Checks TransactionSynchronizationManager: Is a transaction already active?
2. Calls transactionStatus.createAndHoldSavepoint():
──► Connection connection = getDataSource().getConnection();
──► Savepoint sp = connection.setSavepoint("NESTED_SAVEPOINT_1");
│
▼
[ Target Service Method ]
│
┌────────────────────────────┴────────────────────────────┐
▼ ▼
[ On Success ] [ On Exception ]
connection.releaseSavepoint(sp); connection.rollback(sp);
(Savepoint released; (Rolls back to savepoint;
COMMIT is NOT executed yet!) rollback-only flag is NOT set!)
│ │
└────────────────────────────┬────────────────────────────┘
▼
Outer method finishes ──► connection.commit()
(Physical COMMIT of all remaining successful changes)
Comprehensive Comparison: REQUIRED vs. REQUIRES_NEW vs. NESTED
| Criterion | REQUIRED |
REQUIRES_NEW |
NESTED |
|---|---|---|---|
| Physical Connections | 1 connection | 2 independent connections | 1 connection |
| Commit Timing of Inner Work | Together with parent | Immediate upon inner exit | Together with parent |
| Reaction to Inner Error | Whole transaction marked rollback-only |
Rolls back inner transaction only | Rolls back to Savepoint only |
| If Parent Fails AFTER Inner | Everything rolls back | Inner commit REMAINS in DB | Everything rolls back |
| Hibernate JPA Support | Supported out of the box | Supported out of the box | ⚠️ Disabled by default |
| JTA / Distributed Tx | Supported | Supported | ❌ Unsupported (no 2PC savepoints) |
| Pool Deadlock Risk | None | ⚠️ High under connection contention | None (shares active connection) |
🔴 Senior Level
The JPA / Hibernate L1 Cache Inconsistency Hazard
The primary architectural reason Propagation.NESTED is rarely used in standard Spring Data JPA / Hibernate applications:
A JDBC Savepoint rolls back table rows and lock states in the relational database, but Hibernate’s First-Level Cache (
PersistenceContext/EntityManager) remains unaware of the rollback!
Step-by-Step Entity State Desynchronization Hazard:
1. singleService.processSingleOrder() [NESTED]
2. entityManager.persist(orderItem);
──► Hibernate registers orderItem in PersistenceContext as MANAGED.
3. The method executes entityManager.flush() ──► SQL INSERT sent to DB.
4. Business validation fails ──► Spring executes connection.rollback(savepoint).
──► In the DB, the inserted row is physically obliterated.
5. Outer method catches the exception in try-catch and continues processing.
──► But in JVM heap memory, orderItem remains MANAGED inside Hibernate's L1 cache!
6. Outer method finishes; Spring invokes final flush() and commit():
──► Hibernate sees orderItem in MANAGED state and attempts to emit an UPDATE or second INSERT,
triggering EntityNotFoundException, OptimisticLockException, or foreign key errors!
Safe Production Configuration for NESTED with JPA
If partial rollbacks are strictly required in a JPA-backed application, two mandatory steps must be implemented:
Step 1: Explicitly Enable Savepoints in JpaTransactionManager
By default, JpaTransactionManager throws NestedTransactionNotSupportedException. You must explicitly configure the bean:
@Configuration
public class TransactionConfig {
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
JpaTransactionManager txManager = new JpaTransactionManager(emf);
// Explicitly permit Savepoint usage with JPA:
txManager.setNestedTransactionAllowed(true);
return txManager;
}
}
Step 2: Manually Clear the L1 Cache (EntityManager.clear()) on Exception
To detach corrupted entities after a savepoint rollback, the caller must purge the persistence context:
@Service
public class SafeNestedProcessor {
@PersistenceContext
private final EntityManager entityManager;
private final NestedItemService nestedItemService;
public SafeNestedProcessor(EntityManager entityManager, NestedItemService nestedItemService) {
this.entityManager = entityManager;
this.nestedItemService = nestedItemService;
}
@Transactional
public void processSafeBatch(List<ItemDto> items) {
for (ItemDto item : items) {
try {
nestedItemService.processItem(item); // NESTED
} catch (Exception ex) {
// MANDATORY: Clear L1 cache to evict entities that were rolled back in DB
entityManager.clear();
}
}
}
}
4 Tricky Questions
1. What happens if the parent transaction rolls back AFTER an inner Propagation.NESTED method successfully completed?
Answer:
All changes made by the successful nested method are completely rolled back.
Unlike Propagation.REQUIRES_NEW (which commits permanently in a separate physical connection upon exiting the inner method), Propagation.NESTED retains all mutations within the same open physical connection. When the outer transaction aborts, it executes a global connection.rollback(). This obliterates the entire transaction’s modifications, regardless of whether internal savepoints were released cleanly.
2. Why does executing Propagation.NESTED in a standard Spring Boot + Hibernate application throw NestedTransactionNotSupportedException?
Answer:
Because in Spring’s JpaTransactionManager, the property nestedTransactionAllowed defaults to false.
The Spring team intentionally disabled NESTED for JPA by default to protect applications from entity memory desynchronization. Because Hibernate’s L1 cache cannot be rolled back to match a database savepoint, enabling savepoints without manually invoking entityManager.clear() causes persistent cache corruption. For safe out-of-the-box NESTED execution, developers should use DataSourceTransactionManager with JDBC, Spring JdbcTemplate, jOOQ, or MyBatis.
3. Are database row locks released when rolling back to a savepoint (ROLLBACK TO SAVEPOINT)?
Answer:
Yes, row locks acquired after the savepoint was created are released.
When the database engine (PostgreSQL, MySQL InnoDB, Oracle) executes ROLLBACK TO SAVEPOINT sp:
- All mutations executed after savepoint
spare reversed via the undo/WAL logs. - All exclusive row locks (
X-locks) acquired byUPDATE,DELETE, orSELECT ... FOR UPDATEstatements executed after savepointspare immediately released. - Locks acquired by the outer transaction prior to creating savepoint
spare preserved and remain held until the overall transaction commits or aborts.
4. What is the fundamental difference in error handling between REQUIRED and NESTED when an exception is caught in a try-catch block?
Answer:
- Under
REQUIRED: The inner method runs in the shared transaction. When an exception occurs, the inner proxy’sTransactionInterceptorunconditionally marks the transaction status assetRollbackOnly(). Even if the parent catches the exception in atry-catchblock, the outer commit will fail with anUnexpectedRollbackException, forcing a full rollback. - Under
NESTED: The inner proxy’sTransactionInterceptorexecutesconnection.rollback(savepoint)and does NOT mark the outer transaction asrollback-only. The outer method catches the exception cleanly intry-catch, and the parent transaction can commit its own remaining operations without issue.
🎯 Interview Cheat Sheet
- Core Concept: Creates a nested logical transaction via a JDBC Savepoint within the existing physical database transaction.
- Connection Usage: Uses exactly 1 physical database connection (zero extra pool overhead, zero pool deadlock risk).
- Inner Failure Handling: Rolls back only to the savepoint; does not mark the global transaction as
rollback-only. - Outer Failure Handling: If the outer transaction fails, everything is rolled back, including successful nested work.
- JPA Incompatibility:
JpaTransactionManagerdisablesNESTEDby default because Hibernate’s L1 Cache cannot rollback its in-memory entity graph to match savepoint rollbacks. - JTA Limitation: Distributed XA transactions (
JtaTransactionManager) do not support savepoints.