💳 Section 11 · Question #14

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-catch block, 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 sp are reversed via the undo/WAL logs.
  • All exclusive row locks (X-locks) acquired by UPDATE, DELETE, or SELECT ... FOR UPDATE statements executed after savepoint sp are immediately released.
  • Locks acquired by the outer transaction prior to creating savepoint sp are 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’s TransactionInterceptor unconditionally marks the transaction status as setRollbackOnly(). Even if the parent catches the exception in a try-catch block, the outer commit will fail with an UnexpectedRollbackException, forcing a full rollback.
  • Under NESTED: The inner proxy’s TransactionInterceptor executes connection.rollback(savepoint) and does NOT mark the outer transaction as rollback-only. The outer method catches the exception cleanly in try-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: JpaTransactionManager disables NESTED by 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.