Why does Propagation.NESTED often fail with JPA/Hibernate, and what transaction manager makes it work?
answer
- NESTED == JDBC savepoint feature
- JpaTransactionManager => NestedTransactionNotSupportedException
- DataSourceTransactionManager / JdbcTransactionManager = OK
- SavepointManager + useSavepointForNestedTransaction()
- ORM 1st-level cache not reset on savepoint rollback
basics
~10 sNESTED needs JDBC savepoints. JpaTransactionManager does not support them by default, so it throws NestedTransactionNotSupportedException. Use DataSourceTransactionManager (or JdbcTransactionManager), which builds on plain JDBC connections and supports savepoints.
solid answer
~40 sNESTED is implemented via JDBC savepoints, so the active PlatformTransactionManager must be savepoint-capable — its transaction object must implement SavepointManager and useSavepointForNestedTransaction() must return true. DataSourceTransactionManager and JdbcTransactionManager satisfy this because they manage raw JDBC connections. JpaTransactionManager does not enable savepoints by default: nestedTransactionAllowed is false and the JpaDialect generally can't hand back a savepoint handle, so you get NestedTransactionNotSupportedException ('JpaDialect does not support savepoints'). A further trap: even if savepoints worked, Hibernate's first-level cache and write-behind flushing mean the persistence context isn't automatically reset to the savepoint state, so entity state can drift from the database after a savepoint rollback. In practice, if you're on JPA and need partial rollback semantics, reach for REQUIRES_NEW or restructure, rather than forcing NESTED.
code
java · 24 lines@Configuration
public class TxConfig {
// Savepoint-capable: NESTED works here
@Bean
public PlatformTransactionManager jdbcTxManager(DataSource ds) {
return new DataSourceTransactionManager(ds);
}
// JpaTransactionManager: hitting NESTED throws
// NestedTransactionNotSupportedException by default
// @Bean
// public PlatformTransactionManager jpaTxManager(EntityManagerFactory emf) {
// return new JpaTransactionManager(emf);
// }
}
@Service
class Importer {
// Route this to the JDBC manager if multiple managers exist
@Transactional(transactionManager = "jdbcTxManager",
propagation = Propagation.NESTED)
public void tryRow(Row r) { /* savepoint set here */ }
}go deeper
Awareness only: NESTED needs a JDBC-based manager and can fail with JPA.
Should name the exception and the correct manager (DataSourceTransactionManager).
Should explain the SavepointManager requirement and why JpaTransactionManager lacks it.
Should discuss the ORM persistence-context/savepoint mismatch and recommend REQUIRES_NEW or a JDBC path instead of forcing NESTED.
## Root cause: NESTED is a JDBC savepoint feature Spring implements `Propagation.NESTED` by setting a **JDBC savepoint** (`java.sql.Connection.setSavepoint()`) on the current connection. For this to happen, the `PlatformTransactionManager` must be savepoint-capable: - `AbstractPlatformTransactionManager.useSavepointForNestedTransaction()` must return `true`. - The transaction object must implement `org.springframework.transaction.SavepointManager` (and `isNestedTransactionAllowed()` must be true). `org.springframework.jdbc.datasource.DataSourceTransactionManager` (and the newer `JdbcTransactionManager`) satisfy this: they operate directly on a JDBC `Connection`, and their transaction object (`DataSourceTransactionObject`) is a `SavepointManager`. So NESTED "just works" there. ## Why JpaTransactionManager fails `org.springframework.orm.jpa.JpaTransactionManager` manages an `EntityManager`/persistence context, not a raw JDBC connection. By default: - Its `nestedTransactionAllowed` flag is `false`. - Obtaining a savepoint is delegated to the `JpaDialect`. The default dialect (and most providers) cannot produce a savepoint handle through the JPA abstraction, so `JpaDialect.getJdbcConnection(...)`/savepoint support is unavailable. Reaching a NESTED boundary under `JpaTransactionManager` therefore throws `org.springframework.transaction.NestedTransactionNotSupportedException` with a message like *"JpaDialect does not support savepoints - check your JPA provider's capabilities"*. You can call `setNestedTransactionAllowed(true)` on the manager, and some dialects (e.g., `HibernateJpaDialect`) can obtain the underlying JDBC connection to set a savepoint. But it is fragile and provider-dependent, so it is not the recommended path. ## The deeper semantic problem with ORM + savepoints Even where the plumbing allows a savepoint, an ORM adds a **persistence context (first-level cache)** with **write-behind** semantics: SQL is flushed to the DB lazily, often only at commit or when a query forces a flush. A JDBC `rollback(savepoint)` reverts the **database**, but Hibernate's in-memory entity state and pending action queue are **not automatically synchronized** back to the savepoint. Result: the persistence context can hold entities that no longer match the database, causing subtle bugs. This mismatch is the real reason NESTED and JPA are an uneasy pair, beyond the missing savepoint handle. ## What to do instead on JPA - If the inner work must be independently durable: use `REQUIRES_NEW` (separate physical transaction, works with `JpaTransactionManager`). - If you truly need savepoints for partial rollback: use a JDBC-based manager (`DataSourceTransactionManager`) for that path, or drop to `JdbcTemplate`/direct JDBC for the savepoint-sensitive operation. - Often the cleaner design is per-item idempotent processing with retries, or validating/pre-filtering bad items so partial rollback isn't needed at all. ## Related managers - `JtaTransactionManager`: also does not support savepoints (global/distributed transactions), so NESTED is unsupported there too. - Reactive (`R2dbcTransactionManager`): savepoint support exists in R2DBC but that's a different stack from imperative NESTED. ## Gotcha checklist - Configuring both a JPA manager and a JDBC manager? Make sure the NESTED path actually runs under the savepoint-capable one (`@Transactional(transactionManager = "...")`). - The exception is thrown at the boundary, not at startup — integration tests are how you catch it.
- Exactly which exception, and from where, signals that NESTED isn't supported?org.springframework.transaction.NestedTransactionNotSupportedException, thrown at the transaction boundary when the active PlatformTransactionManager isn't savepoint-capable — e.g. JpaTransactionManager reporting 'JpaDialect does not support savepoints'.
- Even if savepoints worked under Hibernate, why can NESTED still be dangerous?A JDBC rollback-to-savepoint reverts the database, but Hibernate's first-level cache and pending flush queue aren't rolled back with it, so in-memory entity state can diverge from the DB, producing stale or inconsistent data.
saying these in an interview costs you the question
- Believing propagation behaves identically regardless of the transaction manager
- Thinking JpaTransactionManager supports NESTED out of the box
- Assuming a savepoint rollback also resets Hibernate's persistence context