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(orfinal): 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.
| Propagation | Behaviour |
|---|---|
| REQUIRED (default) | join the current transaction, or start one |
| REQUIRES_NEW | suspend the current one and start a new independent transaction (e.g. audit log that must commit even if the order fails) |
| NESTED | savepoint inside the current transaction; can roll back to it (JDBC only) |
| MANDATORY | must run inside an existing transaction, else exception |
| SUPPORTS | join if one exists, otherwise run without |
| NOT_SUPPORTED | suspend any transaction and run without |
| NEVER | throw if a transaction exists |
Explain isolation levels and the problems they prevent.
| Level | Dirty read | Non-repeatable read | Phantom read |
|---|---|---|---|
| READ_UNCOMMITTED | possible | possible | possible |
| READ_COMMITTED | prevented | possible | possible |
| REPEATABLE_READ | prevented | prevented | possible* |
| SERIALIZABLE | prevented | prevented | prevented |
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@Transactionalon write methods.
Optimistic vs pessimistic locking?
| Optimistic | Pessimistic | |
|---|---|---|
| How | @Version column; UPDATE ... WHERE id=? AND version=? | SELECT ... FOR UPDATE row lock |
| Conflict | detected at commit: OptimisticLockException (Spring: ObjectOptimisticLockingFailureException) | other transactions wait (or time out) |
| Best for | low contention, long user think time, REST APIs | high 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.