Question 17 · Section 5

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!

Language versions: English Russian Ukrainian

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

  1. REQUIRED - by default
  2. rollbackFor = Exception.class - always explicit
  3. readOnly = true - for reads
  4. Avoid REQUIRES_NEW without reason
  5. Do not make long transactions
  6. TransactionalEventListener -> for post-commit events
  7. 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]]