Transactions and Locking

How does @Transactional work internally?

Spring wraps the bean in a proxy. On a call through the proxy the TransactionInterceptor asks the JpaTransactionManager to begin (or join) a transaction, binds the EntityManager to the thread, calls your method, then commits (flushing changes) or rolls back.

By default it rolls back on unchecked exceptions (RuntimeException, Error) only. For checked exceptions use @Transactional(rollbackFor = Exception.class).

Why does @Transactional sometimes not work?

  • Self-invocation: this.placeOrder() from the same class bypasses the proxy. Move the method to another bean or inject the proxy.
  • The method is private (or final): proxies cannot intercept it.
  • The class is not a Spring bean (created with new).
  • The exception is checked, or is caught inside the method and not rethrown.
  • Wrong annotation: use org.springframework.transaction.annotation.Transactional (it supports propagation, readOnly, rollbackFor).

Explain transaction propagation types.

PropagationBehaviour
REQUIRED (default)join the current transaction, or start one
REQUIRES_NEWsuspend the current one and start a new independent transaction (e.g. audit log that must commit even if the order fails)
NESTEDsavepoint inside the current transaction; can roll back to it (JDBC only)
MANDATORYmust run inside an existing transaction, else exception
SUPPORTSjoin if one exists, otherwise run without
NOT_SUPPORTEDsuspend any transaction and run without
NEVERthrow if a transaction exists

Explain isolation levels and the problems they prevent.

LevelDirty readNon-repeatable readPhantom read
READ_UNCOMMITTEDpossiblepossiblepossible
READ_COMMITTEDpreventedpossiblepossible
REPEATABLE_READpreventedpreventedpossible*
SERIALIZABLEpreventedpreventedprevented

Defaults: PostgreSQL/Oracle/SQL Server use READ_COMMITTED, MySQL InnoDB uses REPEATABLE_READ (*which also blocks most phantoms). Set it with @Transactional(isolation = Isolation.SERIALIZABLE). None of these alone prevents a lost update; use locking for that.

What does @Transactional(readOnly = true) do?

  • Spring sets the Hibernate session to read-only and flush mode MANUAL: no dirty-checking snapshots, no flush, less memory and CPU.
  • The JDBC connection is marked read-only, which some drivers/databases optimise.
  • Lets a routing DataSource send the query to a read replica.
  • A common pattern: @Transactional(readOnly = true) on the service class, plain @Transactional on write methods.

Optimistic vs pessimistic locking?

OptimisticPessimistic
How@Version column; UPDATE ... WHERE id=? AND version=?SELECT ... FOR UPDATE row lock
Conflictdetected at commit: OptimisticLockException (Spring: ObjectOptimisticLockingFailureException)other transactions wait (or time out)
Best forlow contention, long user think time, REST APIshigh contention on hot rows (stock, wallet balance)
@Version
private Long version;                        // optimistic

@Lock(LockModeType.PESSIMISTIC_WRITE)        // pessimistic
@Query("select p from Product p where p.id = :id")
Optional<Product> findByIdForUpdate(@Param("id") Long id);

When does Hibernate flush? Is flush() the same as commit?

Flush sends pending INSERT/UPDATE/DELETE statements to the database; commit makes them permanent. A flushed change can still be rolled back.

With the default FlushMode.AUTO, Hibernate flushes: before commit, before a JPQL/HQL query that touches dirty tables, before native queries (Hibernate native API), and when you call flush(). FlushMode.COMMIT flushes only at commit.

Tip: Insert order at flush is fixed by action type (inserts, updates, collection changes, deletes), not by the order of your calls. That is why a unique-key "delete then insert" can fail without an explicit flush in between.