💳 Section 11 · Question #18

What is Rollback in Transactions

A Rollback is a fundamental database management operation that aborts the active transaction, completely undoing all data mutations executed within it and returning the database...


🟢 Junior Level

A Rollback is a fundamental database management operation that aborts the active transaction, completely undoing all data mutations executed within it and returning the database to the exact consistent state it occupied before BEGIN or START TRANSACTION was issued.

The Core Concept in 30 Seconds

Rollback is the direct realization of Atomicity (the “A” in ACID):

“All operations within a transaction must either be permanently committed together (COMMIT), or upon the slightest failure, completely reversed (ROLLBACK) as if they had never occurred.”

BEGIN;
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE id = 2;

-- If an error occurs (e.g., negative balance check-constraint fails):
ROLLBACK;
-- Both accounts are restored to their original balances instantaneously!

Automatic Rollback in Spring Boot

In Spring Framework, the @Transactional annotation intercepts unhandled exceptions through an AOP proxy and instructs the underlying transaction manager to issue a rollback:

@Service
public class PaymentProcessingService {

    private final WalletRepository walletRepository;

    public PaymentProcessingService(WalletRepository walletRepository) {
        this.walletRepository = walletRepository;
    }

    @Transactional // Defaults to rollback on RuntimeException and Error
    public void processPayment(Long walletId, BigDecimal amount) {
        Wallet wallet = walletRepository.findById(walletId).orElseThrow();

        wallet.setBalance(wallet.getBalance().subtract(amount));
        walletRepository.save(wallet);

        if (wallet.getBalance().compareTo(BigDecimal.ZERO) < 0) {
            // Throwing an exception triggers an automatic connection.rollback()!
            throw new IllegalStateException("Wallet balance cannot be negative!");
        }
    }
}

The Primary Spring Default Rule

[!WARNING] By default, Spring triggers a ROLLBACK strictly for unchecked exceptions (RuntimeException) and system errors (Error). If a method throws a checked exception (IOException, SQLException, or custom subclasses of java.lang.Exception), Spring interprets it as an anticipated business alternative and executes a COMMIT!


🟡 Middle Level

Rollback Trigger Mechanisms in Spring

1. Declarative Rollback (Via Exceptions)

The standard, most expressive approach:

@Transactional(rollbackFor = Exception.class)
public void executeOrder() throws Exception { ... }

2. Programmatic Rollback (Without Throwing Exceptions)

When business requirements require cancelling database writes while returning a structured error response to the client (avoiding HTTP 500 exceptions):

@Service
public class SafeOrderService {

    @Transactional
    public OrderResponse placeOrderSafely(OrderRequest request) {
        saveOrder(request);

        boolean paymentSuccess = externalGateway.charge(request.getAmount());
        if (!paymentSuccess) {
            // Programmatically mark the transaction for rollback without raising an exception:
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            return new OrderResponse(false, "Payment declined by issuing bank");
        }

        return new OrderResponse(true, "Order completed successfully");
    }
}

The Rollback-Only Flag and UnexpectedRollbackException

When transactional methods are composed under default Propagation.REQUIRED:

  1. Method outer() invokes method inner().
  2. Both methods participate in the same underlying physical database transaction.
  3. Method inner() throws a RuntimeException.
  4. The inner proxy’s TransactionInterceptor catches the exception and invokes transactionStatus.setRollbackOnly().
  5. If outer() catches this exception in a try-catch block and attempts to complete cleanly, Spring inspects the transaction status at the outer boundary.
  6. Seeing isRollbackOnly() == true, Spring executes connection.rollback() and throws UnexpectedRollbackException to prevent the caller from mistakenly assuming the operation succeeded.
[ outer() REQUIRED ] ───────────────────────────┐
     │                                           │ (Shared physical transaction)
     ▼                                           │
[ inner() REQUIRED ]                             │
     │                                           │
     ├── Throws RuntimeException!                │
     └── TransactionStatus.setRollbackOnly() ────┘
     │
[ outer() catches error in try-catch and attempts clean exit ]
     │
     ▼
Spring detects Rollback-Only flag:
──► connection.rollback()
──► throw new UnexpectedRollbackException("Transaction silently rolled back because it has been marked as rollback-only");

🔴 Senior Level

Physical Rollback Execution in DBMS Kernels: PostgreSQL vs. MySQL

Different relational database engines implement transaction rollbacks with fundamentally different algorithmic complexity:

Engine Rollback Complexity Physical Engine Mechanism Performance Impact
PostgreSQL Logical Rollback: $O(1)$ Writes TRANSACTION_STATUS_ABORTED (2 bits) into CLOG (pg_xact) and logs XACT_ABORT to WAL. Row locks are released in RAM. Rollback completes in sub-milliseconds, regardless of whether 10 rows or 10,000,000 rows were modified. Dead tuples are cleaned up later by autovacuum.
MySQL (InnoDB) Physical Rollback: $O(N)$ Traverses the transaction’s Undo Log records in reverse order, physically restoring historical column values across data pages in the buffer pool and Clustered Index (B+Tree). Rollback duration is proportional to the number of mutations. Rolling back a bulk update of 10 million rows can paralyze MySQL for several minutes!

The JVM Heap / Hibernate L1 Cache Inconsistency Trap

The most subtle architectural hazard of transaction rollbacks in Java applications:

The database cleanly reverts row state on disk, but the Java Virtual Machine DOES NOT revert field values of objects in the JVM Heap!

@Transactional
public void modifyAccount(Long accountId) {
    Account account = accountRepository.findById(accountId).orElseThrow();
    account.setBalance(BigDecimal.ZERO); // Heap object mutated in RAM!

    // Suppose an unexpected database constraint or exception occurs here:
    throw new RuntimeException("Database error");
}

What happens:

  1. The database executes ROLLBACK: in the database table, balance remains $10,000.
  2. Inside the JVM heap, the Java account instance still has balance == 0!
  3. Hibernate’s PersistenceContext (L1 Cache) is now desynchronized from the actual database state.
  4. If this EntityManager instance is reused (such as in batch jobs or background tasks), subsequent reads will retrieve stale, corrupted entity data from the L1 cache.
  5. Senior Solution: After an unexpected rollback, the active Hibernate session must be cleared via entityManager.clear() or discarded completely.

Non-Transactional Side Effects

// 💥 HAZARDOUS PRODUCTION PATTERN:
@Transactional
public void processOrder(Order order) {
    orderRepository.save(order);
    emailService.sendConfirmationEmail(order); // Network I/O!
    paymentGateway.chargeCard(order);          // 💥 If this fails:
    // Database rolls back, BUT the user ALREADY received the confirmation email!
}

[!TIP] Production Fix: Defer external side effects (emails, message queue dispatches) until the transaction has committed successfully using @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) or TransactionSynchronizationManager.registerSynchronization().


4 Tricky Questions

1. What happens to auto-increment sequences (SEQUENCE in PostgreSQL / AUTO_INCREMENT in MySQL) upon a ROLLBACK?

Answer: Auto-increment sequence values are NEVER rolled back. Database sequence generators operate outside the transactional MVCC model:

  • Calling nextval('seq') atomically advances the sequence counter in shared memory using short-lived spinlocks and immediately commits the new value.
  • If a transaction that consumed ID 105 rolls back, ID 105 is permanently lost. The subsequent transaction receives ID 106.
  • This creates permanent “gaps” in primary key numbering (e.g., IDs 1, 2, 5, 8). This is standard and intentional in relational databases. Enforcing strictly contiguous sequence numbers would require locking the entire table exclusively until transaction commit, destroying concurrent write throughput.

2. Why does rolling back a 1,000,000-row update take 1 millisecond in PostgreSQL, but several minutes in MySQL InnoDB?

Answer:

  • In PostgreSQL, rollback is purely logical ($O(1)$). The backend simply sets 2 bits in the commit log (pg_xact) to TRANSACTION_STATUS_ABORTED and appends an abort record to WAL. The database does not touch the mutated table pages. The newly written tuples simply become invisible to other transactions under MVCC rules, and their physical cleanup is deferred to the asynchronous autovacuum process.
  • In MySQL InnoDB, rollback is an active physical undo operation ($O(N)$). The storage engine must sequentially traverse the transaction’s Undo Log chain in reverse and physically overwrite modified B+Tree pages in the buffer pool with their original column values. For 1,000,000 rows, InnoDB performs 1,000,000 page modifications, saturating CPU and disk I/O.

3. How does ROLLBACK affect entity versioning (@Version / Optimistic Locking) in Spring Data JPA?

Answer: Upon transaction rollback, the @Version field of in-memory Java entity instances is not restored to its previous value. When Hibernate prepares an entity for update, it pre-increments the @Version attribute in memory from 1 to 2. If an exception triggers a rollback:

  • In the database, the record retains version 1.
  • In the JVM heap, the entity object retains version 2.
  • If application code attempts to persist this same entity instance in a subsequent transaction, Hibernate generates UPDATE ... WHERE id = ? AND version = 2. Because the database row still has version 1, zero rows are updated, and Hibernate throws a spurious OptimisticLockException (StaleObjectStateException). Entities involved in rolled-back transactions must be evicted and re-read from the database.

4. What happens if an exception is caught in a try-catch block inside a @Transactional method without re-throwing it or taking further action?

Answer: A Silent Commit occurs! Spring’s AOP proxy intercepts exceptions only when they propagate out of the method boundary. If application code catches and swallows an exception (catch (Exception e) { log.error("Ignored", e); }), Spring assumes the business operation completed successfully. The interceptor invokes connection.commit(). Mutations executed prior to the error are permanently persisted to the database, potentially leaving business entities in a corrupt, half-finished state.


🎯 Interview Cheat Sheet

  • Core Definition: Reverts all uncommitted changes of a transaction to enforce ACID Atomicity.
  • Spring Default Policy: Automatically rolls back on RuntimeException and Error; commits on checked exceptions (Exception).
  • Rollback-Only Flag: If an inner REQUIRED method fails, it marks the transaction rollback-only. Outer try-catch handling will fail at commit with UnexpectedRollbackException.
  • Programmatic Rollback: Invoke TransactionAspectSupport.currentTransactionStatus().setRollbackOnly().
  • Engine Mechanics:
    • PostgreSQL: Logical $O(1)$ abort via CLOG status update; cleanup deferred to autovacuum.
    • MySQL InnoDB: Physical $O(N)$ undo log replay overwriting clustered index pages.
  • Sequence Gaps: Sequence counters (nextval) are non-transactional and never roll back.
  • Heap Desynchronization: JVM heap objects retain their mutated values after a database rollback; call entityManager.clear() to prevent corrupted L1 reads.