REQUIRED vs REQUIRES_NEW, rolling back "nested" transactions and how Spring simulates nesting (savepoints), isolation levels in JPA and the database defaults, @Transactional on private methods and on interfaces, checked vs unchecked exception rollback and rollbackFor, when a transaction really begins and commits in Spring Data JPA, the EntityManager in the transaction lifecycle, dirty checking and flush modes, read-only transactions that don't flush, transaction timeouts and deadlock detection, and LazyInitializationException causes.
Published September 25, 2026
Transaction bugs are silent data bugs:
Tie every answer to the proxy (where the transaction starts), the persistence context (when the SQL is actually issued), and the database (the isolation behaviour).
Propagation.REQUIRED and REQUIRES_NEW?Short answer:
REQUIRED (the default): join the current transaction if one exists, otherwise start one. Everything commits or rolls back together. An exception in the inner method marks the shared transaction rollback-only.REQUIRES_NEW: suspend the current transaction, and start an independent one. It commits or rolls back on its own, regardless of the outer transaction's outcome. It's used for audit logs, error records, or outbox and ID allocation that must persist even if the main work fails.The costs of REQUIRES_NEW:
Learn it in depth → @Transactional Deep Dive
Short answer: JPA has no true nested transactions.
REQUIRED, there's one physical transaction. An inner failure marks it rollback-only. If the outer code catches the exception and tries to commit, Spring throws UnexpectedRollbackException, and everything is rolled back.Propagation.NESTED simulates nesting with JDBC savepoints. The inner scope rolls back to the savepoint, and the outer transaction can continue. It's supported by DataSourceTransactionManager/JdbcTransactionManager, but not by JpaTransactionManager (Hibernate's persistence context can't be partially rolled back cleanly).REQUIRES_NEW provides independent transactions (not nested ones). The inner commit survives an outer rollback, and vice versa.Short answer: JPA itself defines no isolation API. The isolation is the JDBC connection's isolation level, set by:
@Transactional(isolation = Isolation.REPEATABLE_READ) (Spring sets it on the connection; JpaTransactionManager supports it through the JPA dialect);The levels: READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ and SERIALIZABLE (with DEFAULT meaning "use the database's default").
The database defaults: PostgreSQL, Oracle and SQL Server use READ COMMITTED. MySQL InnoDB uses REPEATABLE READ.
Beyond the database:
@Version) handles lost updates without raising the isolation level;SELECT … FOR UPDATE) protect specific rows.Common trap: higher isolation levels reduce concurrency, and cause more serialisation failures and deadlocks, which you must retry.
Learn it in depth → Transactions & Isolation Levels
@Transactional on a private method mean?Short answer: Nothing: it's ignored. Spring applies @Transactional through proxies, which can only intercept calls from outside the bean to proxyable (public, or with CGLIB, protected and package-private) methods. A private method is never intercepted, so no transaction is created. Since Spring 6, class-based proxies also advise protected and package-private methods, but never private ones, and never self-invocations. Fix it by moving the method to a separate bean, making it public and calling it through the proxy, using TransactionTemplate, or using AspectJ mode.
@Transactional be used on an interface, or on interface methods?Short answer: It works in current Spring versions: annotations on interface methods are detected for both JDK and CGLIB proxies (since Spring 5 or 6). But the recommendation is to put @Transactional on concrete classes and methods:
Spring Data repositories already declare transactional defaults on SimpleJpaRepository: readOnly = true on reads, and read-write on save and delete.
rollbackFor do? What happens when an inner transactional method throws a checked exception?Short answer: By default, Spring rolls back only on unchecked exceptions (RuntimeException, Error), and commits on checked exceptions. That's inherited from EJB conventions, on the idea that checked exceptions represent expected business outcomes. So when an inner @Transactional method throws a checked exception (say InsufficientFundsException extends Exception), the transaction isn't marked rollback-only, and the changes made before it commit, unless you configure it:
rollbackFor = Exception.class (or specific types) rolls back on those checked exceptions too;noRollbackFor = ... keeps the commit, for specific runtime exceptions;@EnableTransactionManagement(rollbackOn = RollbackOn.ALL_EXCEPTIONS).The practice: use unchecked domain exceptions, or set rollbackFor consistently (through a meta-annotation like @BusinessTransactional).
Short answer:
It begins when a call enters a @Transactional proxy boundary: your service method, or, if there's none, the repository method itself, since SimpleJpaRepository methods are transactional. The JpaTransactionManager binds an EntityManager and a JDBC connection to the thread. With DataSources configured for it, the connection may be acquired lazily (LazyConnectionDataSourceProxy, or Hibernate's DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION handling mode).
SQL is not executed immediately on save(): inserts and updates are queued in the persistence context, and flushed:
FlushModeType.AUTO);flush()/saveAndFlush().Identity-generated IDs (GenerationType.IDENTITY) force an immediate INSERT, to obtain the ID.
It commits when the outermost transactional method returns normally: flush, then JDBC commit, then @TransactionalEventListener(AFTER_COMMIT) callbacks. It rolls back when a rollback-triggering exception propagates out.
Common trap: without a service-level @Transactional, each repository call is its own transaction, so a multi-step operation is not atomic.
EntityManager's role in the transaction lifecycle?Short answer: The EntityManager is the persistence context, the unit of work:
persist, find, merge, remove) and their snapshots;@PersistenceContext), which delegates to the EntityManager bound to the current transaction;flush() synchronises with the database (without committing), clear() detaches everything (useful in batches), and refresh() reloads the database state.
Short answer: When an entity becomes managed (loaded or persisted), Hibernate keeps a snapshot of its state. At flush time, it compares every managed entity with its snapshot (or uses enhanced dirty tracking), and issues UPDATE statements for the changed ones, without any explicit save() call. The flush is triggered:
flush().The implications:
clear() or StatelessSession, or read-only mode);@DynamicUpdate updates only the changed columns (a trade-off: no cached SQL);Short answer:
@Transactional(readOnly = true): Spring passes the hint through to Hibernate, which sets the session's FlushMode.MANUAL (no automatic flush) and read-only mode for loaded entities (no snapshots, and no dirty checking, which saves memory and CPU). With some drivers and pool setups, it also sets Connection.setReadOnly(true) (it can route to replicas, or enable database optimisations).@QueryHints(@QueryHint(name = HibernateHints.HINT_READ_ONLY, value = "true")), or session.setDefaultReadOnly(true).Common trap: readOnly isn't a security guarantee on every stack. An explicit flush(), or a modifying query, can still write, depending on the provider, and the database connection's read-only flag.
Short answer:
Timeouts: @Transactional(timeout = 5) (seconds). Spring's JPA and JDBC support applies the remaining time as the JDBC statement query timeout on each statement (and checks the deadline between operations), so long-running statements are cancelled, and the transaction rolls back (QueryTimeoutException/TransactionTimedOutException). It doesn't interrupt Java code that isn't talking to the database. Also configure database-level limits (statement_timeout, lock_timeout, idle_in_transaction_session_timeout in Postgres) as a safety net.
Deadlock detection is done by the database:
deadlock_timeout (1 second);innodb_lock_wait_timeout).The database kills a victim transaction with an error, which Spring translates into CannotAcquireLockException/DeadlockLoserDataAccessException. The application should retry the whole transaction (idempotently, with backoff: Spring Retry's @Retryable placed outside @Transactional).
Prevention: acquire locks in a consistent order, keep transactions short, use the right indexes (so fewer rows are locked), and use SKIP LOCKED or NOWAIT for work queues.
LazyInitializationException, and how do you prevent it?Short answer: Causes:
toString/logging after the service returns, and in async threads or scheduled jobs without a transaction;@Cacheable) and used later;@Transactional.Prevention:
@Transactional(readOnly = true) when they navigate associations;enable_lazy_load_no_trans.Q: What is TransactionTemplate, and when do you prefer it?
A: Programmatic transaction boundaries (tx.execute(status -> …)). Prefer it for fine-grained control inside one method: several transactions in one method, conditional rollback (status.setRollbackOnly()), or code that can't be proxied.
Q: How do @TransactionalEventListener phases work?
A: Listeners run relative to the publishing transaction: BEFORE_COMMIT, AFTER_COMMIT (the default), AFTER_ROLLBACK or AFTER_COMPLETION. If there's no transaction, the listener doesn't run unless fallbackExecution = true. Use AFTER_COMMIT for side effects (emails, messages) that must not fire on rollback.
Q: Why might a @Transactional test not reveal a bug?
A: Test-managed transactions roll back at the end, and the persistence context may serve entities from memory, so flush problems, constraint violations or lazy-loading issues can go unnoticed. Call flush() in tests, or run some tests without a test transaction.
Q: What's the difference between persist and merge?
A: persist makes a new instance managed (it fails for detached entities with IDs). merge copies the state of a detached or new instance onto a managed copy, and returns that copy. The argument stays detached, which is a common source of bugs.