What Does the @Transactional Annotation Do?
4. Avoid REQUIRES_NEW without reason 5. Do not make long transactions 6. TransactionalEventListener -> for post-commit events 7. Self-invocation -> does not work!
Junior Level
@Transactional is an annotation that automatically manages DB transactions.
Simple analogy: A bank transfer. Either the money is debited AND credited, or nothing happens. It cannot be that money is debited but not credited.
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) {
orderRepository.save(order); // SQL INSERT
paymentRepository.charge(order); // SQL UPDATE
// If error -> everything will be rolled back!
}
}
How it works:
Proxy -> BEGIN TRANSACTION -> calls method -> COMMIT
| (error)
ROLLBACK
Middle Level
Propagation
| Propagation | Behavior |
|---|---|
| REQUIRED (default) | Uses existing or creates a new one |
| REQUIRES_NEW | Always creates a new one (suspends current) |
| NESTED | Nested via Savepoint |
| MANDATORY | Requires existing (otherwise error) |
Isolation
| Isolation | What it prevents |
|---|---|
| READ_COMMITTED | Dirty Reads |
| REPEATABLE_READ | Non-repeatable Reads |
| SERIALIZABLE | Phantom Reads (but slow!) |
Rollback Rules
// By default: only RuntimeException and Error (unchecked exceptions). Checked exceptions (Exception and below) do NOT cause rollback.
@Transactional
public void save() throws IOException { }
// IOException -> will NOT roll back!
// Explicit specification
@Transactional(rollbackFor = Exception.class)
public void save() throws IOException { }
// IOException -> will roll back!
readOnly Optimization
@Transactional(readOnly = true)
public List<User> findAll() {
// -> Hibernate disables Dirty Checking
// -> Saves CPU and memory
}
Senior Level
REQUIRES_NEW Risk
@Transactional
public void outer() {
inner(); // REQUIRES_NEW -> new transaction!
}
@Transactional(propagation = REQUIRES_NEW)
public void inner() {
// Suspends the outer transaction
// Takes a DIFFERENT connection from pool!
// -> Risk of deadlock with small pool
}
Connection Release Problem
@Transactional
public void process() {
repository.save(order); // Connection taken
httpClient.callExternal(); // 5 seconds -> connection held!
repository.save(payment);
}
// -> Connection can be held for the entire transaction. Behavior depends on ConnectionReleaseMode: by default in Spring + Hibernate, connection is held until commit/rollback.
// Solution: split into two methods
TransactionalEventListener
// Send to Kafka ONLY after commit!
@TransactionalEventListener(phase = AFTER_COMMIT)
public void onOrderCreated(OrderCreatedEvent event) {
kafkaTemplate.send("orders", event);
}
// -> If transaction rolls back -> event is NOT sent!
// -> If the method publishing the event is NOT in a transaction - event is sent immediately, as if it were a regular ApplicationEvent.
Production Experience
Real scenario: long transaction
@Transactional method:
1. Save order (10 ms)
2. External API HTTP call (3 seconds)
3. Save payment (10 ms)
-> Connection busy for 3+ seconds!
-> At 100 RPS -> 300 connections needed!
-> Pool of 20 -> all busy -> timeouts!
Solution: split into two methods
Best Practices
- REQUIRED - by default
- rollbackFor = Exception.class - always explicit
- readOnly = true - for reads
- Avoid REQUIRES_NEW without reason
- Do not make long transactions
- TransactionalEventListener -> for post-commit events
- Self-invocation -> does not work!
Summary for Senior
- Proxy -> TransactionInterceptor manages transaction
- Propagation -> REQUIRED/REQUIRES_NEW/NESTED
- rollbackFor -> explicitly specify Exception.class
- readOnly -> disables Dirty Checking
- Connection Release -> do not hold long
- TransactionalEventListener -> AFTER_COMMIT
- Self-invocation -> does not work!
Interview Cheat Sheet
Must know:
- @Transactional = automatic transaction management: BEGIN -> method -> COMMIT / ROLLBACK
- By default: propagation = REQUIRED, rollback only for RuntimeException and Error
- Propagation: REQUIRED (uses/creates), REQUIRES_NEW (always new), NESTED (via Savepoint)
- Isolation: READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE - what they prevent
- rollbackFor = Exception.class - always explicitly specify for checked exceptions
- readOnly = true - disables Hibernate Dirty Checking, saves CPU/memory
- REQUIRES_NEW takes a DIFFERENT connection from pool -> deadlock risk with small pool
- TransactionalEventListener(AFTER_COMMIT) - event sent only after commit
- Self-invocation does NOT work - call through this around proxy
Common follow-up questions:
- Which exceptions cause rollback by default? -> Only unchecked (RuntimeException, Error). Checked Exception - do NOT roll back
- How does REQUIRES_NEW differ from NESTED? -> REQUIRES_NEW = separate transaction, NESTED = nested via Savepoint within the same
- Why is REQUIRES_NEW dangerous? -> Takes another connection, with small pool - deadlock/timeout
- What to do with long transactions? -> Split into methods, remove HTTP calls from transaction
Red flags (DO NOT say):
- “@Transactional rolls back all exceptions” - only unchecked by default
- “REQUIRES_NEW = nested transaction” - that is NESTED, REQUIRES_NEW is separate
- “readOnly = true forbids writing” - it is a Hibernate optimization, not a write ban
- “Transaction releases connection between calls” - holds until commit/rollback
Related topics:
- [[13. What is a proxy in Spring]]
- [[18. Why Transactional does not work with self-invocation]]
- [[19. How to solve the self-invocation problem]]
- [[14. When does Spring create a proxy]]