Mastering Transaction Rollbacks in Java Backend Applications

Mastering Transaction Rollbacks in Java Backend Applications

What Are Transactions?
A transaction is a sequence of operations performed as a single logical unit of work. To ensure the consistency of data, transactions adhere to the ACID properties:

  • Atomicity: All operations in a transaction succeed or none do.
  • Consistency: Transactions bring the database from one valid state to another.
  • Isolation: Transactions do not interfere with each other.
  • Durability: Once committed, changes are permanent.

    Handling Rollbacks in Java Backend Applications

    Rollback ensures that the system returns to its previous consistent state when a transaction fails. This section includes code samples and real-world examples demonstrating rollback management in Spring Boot.

    1. Basic Rollback Example
    Here’s how Spring’s @Transactional annotation automatically rolls back transactions when an exception occurs:

    Scenario: Bank Funds Transfer

    A user transfers money from one account to another. If the debit or credit operation fails, the entire transaction should rollback.

    @Service 
    
    public class BankService { 
    
    @Autowired 
    
    private AccountRepository accountRepository;
    
    @Transactional 
    
    public void transferFunds(Long fromAccountId, Long toAccountId, BigDecimal amount) { Account fromAccount = accountRepository.findById(fromAccountId) 
    
    .orElseThrow(() -> new IllegalArgumentException("Invalid sender account ID")); Account toAccount = accountRepository.findById(toAccountId) 
    
    .orElseThrow(() -> new IllegalArgumentException("Invalid receiver account ID")); 
    
    // Debit operation 
    
    fromAccount.debit(amount); 
    
    // Simulate an error during the credit operation 
    
    if (toAccountId == null) { 
    
    throw new IllegalStateException("Receiver account ID cannot be null"); } 
    
    // Credit operation 
    
    toAccount.credit(amount); 
    
    accountRepository.save(fromAccount); 
    
    accountRepository.save(toAccount); 
    
    } 
    
    }

    Explanation:
    ● If an exception (e.g., IllegalStateException) occurs, Spring automatically rolls back the transaction.
    ● Without rollback, one account might be debited without the other being credited, causing inconsistency.

    2. Rollback for Specific Exceptions

    Sometimes, you may want to rollback only for certain exceptions while committing for others. Use the rollbackFor attribute in @Transactional.

    Scenario: Order Placement with Inventory Check

    If the inventory is insufficient, rollback the transaction. For non-critical exceptions like logging failures, don’t rollback.

    @Service 
    
    public class OrderService { 
    
    @Autowired 
    
    private InventoryService inventoryService; 
    
    @Autowired 
    
    private OrderRepository orderRepository; 
    
    @Transactional(rollbackFor = {InventoryException.class}) 
    
    public void placeOrder(Order order) { 
    
    // Deduct inventory 
    
    inventoryService.reduceStock(order.getProductId(), order.getQuantity()); 
    
    // Save the order 
    
    orderRepository.save(order);
    
    // Simulate a logging failure 
    
    try { 
    
    logOrder(order); 
    
    } catch (LoggingException e) { 
    
    // Log the failure but don't rollback the transaction 
    
    System.err.println("Logging failed: " + e.getMessage()); 
    
    } 
    
    } 
    
    private void logOrder(Order order) throws LoggingException { 
    
    throw new LoggingException("Failed to log order details"); 
    
    } 
    
    }

    Explanation:
    ● rollbackFor ensures rollback for InventoryException but commits the transaction if a LoggingException occurs.

    3. Custom Rollback Scenarios

    For advanced cases, you might manually mark a transaction for rollback using TransactionAspectSupport.

    Scenario: Conditional Rollback During Payment Processing
    If the payment gateway fails, rollback the transaction manually.

    @Service 
    
    public class PaymentService { 
    
    @Autowired 
    
    private PaymentRepository paymentRepository; 
    
    @Transactional 
    
    public void processPayment(Payment payment) { 
    
    try { 
    
    // Save payment details 
    
    paymentRepository.save(payment); 
    
    // Simulate a payment gateway call 
    
    boolean paymentSuccess = callPaymentGateway(payment); 
    
    if (!paymentSuccess) { 
    
    // Mark for rollback 
    
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); System.err.println("Payment failed, rolling back transaction..."); } 
    
    } catch (Exception e) { 
    
    throw new RuntimeException("Error during payment processing", e); } 
    
    }
    
    private boolean callPaymentGateway(Payment payment) { 
    
    // Simulate a payment failure 
    
    return false; 
    
    } 
    
    }

    Explanation:
    ● TransactionAspectSupport.currentTransactionStatus().setRollbackOn ly() is used to mark the transaction for rollback explicitly.

    Custom Exceptions
    Here are the custom exceptions used in the examples:

    public class InventoryException extends Exception { 
    public InventoryException(String message) { 
    super(message); 
    } 
    } 
    public class LoggingException extends RuntimeException { 
    public LoggingException(String message) { 
    super(message); 
    } 
    }

Leave a Reply