Question 18 · Section 11

What is rollback in transactions?

Like an "Undo" button (Ctrl+Z) in a text editor — everything you did since the last save is reverted.

Language versions: English Russian Ukrainian

🟢 Junior Level

Rollback is an operation that cancels all changes of the current transaction and returns the database to the state before it started.

Mechanism: The DBMS uses undo log (journal of old values). On rollback, each modified row is restored from undo log. In Spring, TransactionInterceptor calls Connection.rollback(), which signals the DBMS to roll back the current transaction.

Simple example

BEGIN;
UPDATE accounts SET balance = balance - 10 WHERE id = 1;
UPDATE accounts SET balance = balance + 10 WHERE id = 2;
-- Something went wrong!
ROLLBACK;  -- Both changes cancelled, balances returned to original

Analogy

Like an “Undo” button (Ctrl+Z) in a text editor — everything you did since the last save is reverted.

Why it’s needed

Rollback guarantees atomicity (the A in ACID): either all changes are saved, or none at all.

Rollback in Spring

Spring automatically rolls back when a @Transactional method throws an exception.

@Transactional
public void transfer(Long from, Long to, BigDecimal amount) {
    Account fromAcc = repo.findById(from);
    Account toAcc = repo.findById(to);
    fromAcc.setBalance(fromAcc.getBalance().subtract(amount));
    toAcc.setBalance(toAcc.getBalance().add(amount));
    repo.save(fromAcc);
    repo.save(toAcc);
    // If RuntimeException fires here — everything automatically rolls back
}

By default

Spring rolls back only on RuntimeException and Error. Checked Exceptions (Exception subclasses) do NOT trigger rollback — Spring will COMMIT.

Why Spring decided this: RuntimeException = programming error (NPE, IAE), from which recovery is impossible → rollback. Checked Exception = expected business situation (user not found), which the caller can handle → commit.

When NOT to do rollback

  1. Irreversible side effects — email already sent, payment already went through external API. DB rollback won’t cancel external action.
  2. Long transactions with partial success — better to record partial result and handle later than lose everything.
  3. Logging/auditing — this data should survive even if the main transaction rolls back (use REQUIRES_NEW).

🟡 Middle Level

How Rollback works at the DB level

  1. All changes written to Undo Log
  2. On ROLLBACK, DBMS reads Undo Log in reverse order
  3. Compensating actions executed, restoring old values
  4. Locks released only after rollback completes

Rollback in Spring Framework

Automatic (declarative)

Happens when an exception is thrown from a @Transactional method.

Programmatic (manual)

@Transactional
public void someMethod() {
    try {
        // logic
    } catch (Exception e) {
        TransactionAspectSupport.currentTransactionStatus()
            .setRollbackOnly();
    }
}

Rollback-Only State

If in a REQUIRED → REQUIRED call chain one method throws RuntimeException, Spring marks the transaction with a rollback-only flag.

Even if the outer method catches the exception and tries to complete successfully — on exit Spring sees the flag and throws UnexpectedRollbackException.

Partial Rollback (Savepoints)

Through Propagation.NESTED, you can roll back only part of a transaction to a savepoint.

When NOT to use rollback

Situation Why Alternative
Compensating transaction (Saga) Data needed for reverse operation Explicit compensating operation
Audit logging Log must survive even on error REQUIRES_NEW for logs
Partial success in batch One failed item ≠ rollback entire batch Handle error, continue

Common mistakes

Mistake What happens How to fix
Catching and swallowing exception Spring doesn’t see exception → COMMIT Rethrow or setRollbackOnly()
@Transactional without rollbackFor on checked exception COMMIT instead of ROLLBACK rollbackFor = Exception.class
Expecting full rollback in REQUIRED chain Inner method already set rollback-only Use REQUIRES_NEW for independent operations
Rollback and subsequent EntityManager use Session in inconsistent state Don’t reuse EntityManager after rollback

Comparison: automatic vs manual rollback

Approach Pros Cons When to use
Automatic (@Transactional) Clean code, minimal boilerplate Only on exception exit Standard scenarios
Manual (setRollbackOnly) Conditional rollback, no throw Boilerplate, coupling to Spring API Business logic with condition

🔴 Senior Level

Rollback Mechanics — Database Level

PostgreSQL Rollback Process

1. Transaction enters abort state
2. All tuple modifications marked as dead:
   - Inserted tuples: xmin = aborted txid → invisible to all future txns
   - Updated tuples: old version remains (xmax not set), new version marked aborted
   - Deleted tuples: deletion is undone (xmax cleared)
3. Row-level locks released
4. Transaction state changed to ABORTED
5. XLOG record written: XACT_ABORT

Key insight: In PostgreSQL MVCC, rollback is almost entirely logical — tuple versions marked invalid via transaction status. Physical cleanup happens later via VACUUM.

InnoDB Rollback Process

1. Transaction enters rollback state
2. Undo log records read in reverse order
3. For each undo record:
   - Restore old value to data page
   - Release associated locks
   - Update undo log pointer
4. Transaction state → ROLLED BACK
5. Trx sys history updated

InnoDB rollback is more physical — data pages actually restored to previous state via undo log.

Spring Rollback Decision Tree

// Simplified AbstractPlatformTransactionManager
private void processRollback(DefaultTransactionStatus status, Throwable ex) {
    // 1. Check if rollback is actually needed
    if (ex != null && !status.getTransactionAttribute().rollbackOn(ex)) {
        return;  // Don't rollback — will commit instead!
    }

    // 2. Check if this is a nested transaction (savepoint)
    if (status.hasSavepoint()) {
        status.rollbackToHeldSavepoint();  // Rollback to savepoint only
        return;
    }

    // 3. Check if this is a new transaction
    if (status.isNewTransaction()) {
        status.getTransactionManager().doRollback(status);
    }
    // 4. Participating in existing transaction
    else {
        status.setRollbackOnly();  // Can't actually rollback, just mark
    }
}

UnexpectedRollbackException — Full Analysis

@Transactional  // REQUIRED — starts Tx1
public void outer() {
    try {
        inner();  // REQUIRED — joins Tx1
    } catch (RuntimeException e) {
        // Exception caught, but Tx1 already marked rollback-only
    }
    // Spring tries to commit Tx1 → sees rollback-only flag
    // Throws UnexpectedRollbackException
}

@Transactional  // REQUIRED — joins Tx1
public void inner() {
    repo.save(entity);
    throw new RuntimeException();  // TransactionInterceptor → setRollbackOnly()
}

Flow:

  1. outer() starts physical transaction Tx1
  2. inner() joins Tx1 (same connection)
  3. inner() throws RuntimeException
  4. TransactionInterceptor catches, calls setRollbackOnly() on Tx1
  5. outer() catches exception, returns normally
  6. TransactionInterceptor tries to COMMIT Tx1
  7. TransactionManager sees rollback-only flag
  8. Throws UnexpectedRollbackException

Solutions:

// Option 1: Don't catch — let it propagate
@Transactional
public void outer() {
    inner();  // If inner fails — outer rolls back too
}

// Option 2: REQUIRES_NEW for independent operations
@Transactional
public void outer() {
    try {
        independentService.doWork();  // REQUIRES_NEW — separate transaction
    } catch (Exception e) {
        // Safe — independent tx already handled
    }
}

// Option 3: Catch and rethrow
@Transactional
public void outer() {
    try {
        inner();
    } catch (RuntimeException e) {
        throw new BusinessException("Failed", e);  // Still causes rollback
    }
}

Edge Cases (minimum 3)

  1. Rollback and Sequence: SELECT nextval('seq') is NOT rolled back. Sequence advances independently of rollback. After rollback, sequence value already incremented — next call returns next value. This leads to gaps in sequence.

  2. Rollback and Hibernate @Version: Entity version is NOT restored on rollback. Entity considered stale — on subsequent operations OptimisticLockException.

  3. Rollback and L1 Cache: After rollback, Hibernate Persistence Context remains with dirty entities. Entity still in L1 cache, but state doesn’t match DB. Spring creates new EntityManager for next transaction, but if you manually reuse — problem.

  4. Partial flush before rollback: Hibernate may have flushed some changes before exception occurred. At DB level they will be rolled back, but in Hibernate session they remain as persisted.

Performance Numbers: Rollback vs Commit

Operation PostgreSQL InnoDB
Rollback latency O(1) — transaction status changes O(N) — undo each change
Commit latency fsync WAL (~1-5 ms) fsync redo log (~1-5 ms)
Lock release Fast (in-memory) Fast (in-memory)
Connection cleanup ~10-50 μs ~10-50 μs

Rollback in PostgreSQL is faster than commit (no fsync needed). In InnoDB — depends on number of changes.

Memory Implications

  • Undo log: Grows proportionally to transaction changes. Long transaction with large rollback = large undo log.
  • Hibernate L1 cache: After rollback, entities remain in cache — memory not freed until EntityManager closed.
  • Connection pool: Connection returns to pool only after rollback completes. Long rollback = connection occupied longer.

Thread Safety

Rollback always happens in the same thread that executed the transaction. ThreadLocal state (TransactionSynchronizationManager) guarantees isolation. @Async methods have separate transaction context — their rollback doesn’t affect caller.

Production War Story

Payment processing service: outer() called inner() with REQUIRED. inner() failed with DataIntegrityViolationException. outer() caught Exception, logged, and continued — expecting the transaction to be alive. Instead, Spring threw UnexpectedRollbackException on commit. All payments failed with 500. Fix: inner() moved to REQUIRES_NEW — its rollback doesn’t affect outer transaction.

Monitoring

Debug logging:

logging:
  level:
    org.springframework.transaction: DEBUG
    org.hibernate.SQL: DEBUG

Micrometer metrics:

@Bean
public TransactionSynchronization transactionMetrics(MeterRegistry registry) {
    return new TransactionSynchronization() {
        @Override
        public void afterCompletion(int status) {
            if (status == STATUS_ROLLED_BACK) {
                registry.counter("transaction.rollback").increment();
            }
        }
    };
}

Actuator:

GET /actuator/metrics/transaction.rollback
GET /actuator/metrics/transaction.commit

Highload Best Practices

  1. Minimal transaction duration: Longer transaction = larger undo log and higher rollback cost.
  2. Batch processing with savepoints: Split large batch operations into chunks. On one chunk error — rollback only that chunk.
  3. Avoid catch-and-swallow: If you caught Exception — explicitly call setRollbackOnly() or rethrow.
  4. Use REQUIRES_NEW for side-effects: Logging, notifications — shouldn’t block main transaction on rollback.
  5. Monitor rollback rate: High % rollback = problem in business logic or validation, not in transactions.
  6. Test rollback behavior: Integration tests should verify data actually rolled back, not just that exception was thrown.

🎯 Interview Cheat Sheet

Must know:

  • Rollback = cancel all transaction changes, return to state before BEGIN (via undo log)
  • Spring automatically rolls back on RuntimeException and Error, checked exceptions → commit
  • Rollback-only flag: inner REQUIRED method threw RuntimeException → entire transaction marked for rollback
  • UnexpectedRollbackException: outer tries to commit, but transaction already marked rollback-only
  • Savepoints (Propagation.NESTED) allow partial rollback without rolling back entire transaction
  • Sequence values (nextval) are NOT rolled back — leads to gaps in sequence

Common follow-up questions:

  • What happens if you catch exception in @Transactional method? — Spring doesn’t see exception → commit, need setRollbackOnly()
  • How does rollback differ in PostgreSQL vs InnoDB? — PG: logical (tx status), InnoDB: physical (undo log reverse)
  • When is rollback undesirable? — Irreversible side effects (email, external API), partial success in batch
  • How to implement conditional rollback? — TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()

Red flags (DO NOT say):

  • “Rollback rolls back sequence” — nextval is non-transactional, gaps are inevitable
  • “Caught exception = rollback” — Spring doesn’t see caught exceptions
  • “EntityManager can be reused after rollback” — Session in inconsistent state

Related topics:

  • [[19. Which exceptions cause rollback by default]]
  • [[20. How to configure rollback for checked exceptions]]
  • [[14. What does Propagation.NESTED do]]
  • [[1. Decode each letter of ACID]]