Overview
Transaction demarcation defines the boundaries of transactions - where they begin, end, and how they interact with other transactions. Proper transaction management is crucial for data consistency and integrity.
Transaction Demarcation Strategies
- Programmatic Demarcation: Manual transaction control in code
- Declarative Demarcation: Using annotations or configuration
- Container-Managed: Application server handles transactions
Programmatic Transaction Demarcation
1. JDBC Transaction Management
import java.sql.*; public class JdbcTransactionExample { private final String url; private final String user; private final String password; public JdbcTransactionExample(String url, String user, String password) { this.url = url; this.user = user; this.password = password; } // Basic transaction with try-with-resources public void transferMoney(int fromAccount, int toAccount, double amount) throws SQLException { Connection conn = null; try { conn = DriverManager.getConnection(url, user, password); conn.setAutoCommit(false); // Start transaction // Withdraw from source account withdraw(conn, fromAccount, amount); // Deposit to target account deposit(conn, toAccount, amount); conn.commit(); // Commit transaction System.out.println("Transfer completed successfully"); } catch (SQLException e) { if (conn != null) { conn.rollback(); // Rollback on error System.out.println("Transfer failed - transaction rolled back"); } throw e; } finally { if (conn != null) { conn.setAutoCommit(true); conn.close(); } } } // Using try-with-resources for better resource management public void transferMoneyWithResources(int fromAccount, int toAccount, double amount) throws SQLException { try (Connection conn = DriverManager.getConnection(url, user, password)) { conn.setAutoCommit(false); try { withdraw(conn, fromAccount, amount); deposit(conn, toAccount, amount); conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } } } private void withdraw(Connection conn, int accountId, double amount) throws SQLException { String sql = "UPDATE accounts SET balance = balance - ? WHERE id = ? AND balance >= ?"; try (PreparedStatement stmt = conn.prepareStatement(sql)) { stmt.setDouble(1, amount); stmt.setInt(2, accountId); stmt.setDouble(3, amount); int rowsAffected = stmt.executeUpdate(); if (rowsAffected == 0) { throw new SQLException("Insufficient funds or account not found"); } } } private void deposit(Connection conn, int accountId, double amount) throws SQLException { String sql = "UPDATE accounts SET balance = balance + ? WHERE id = ?"; try (PreparedStatement stmt = conn.prepareStatement(sql)) { stmt.setDouble(1, amount); stmt.setInt(2, accountId); stmt.executeUpdate(); } } // Transaction with savepoints public void complexOperationWithSavepoints() throws SQLException { try (Connection conn = DriverManager.getConnection(url, user, password)) { conn.setAutoCommit(false); Savepoint savepoint1 = null; try { // Step 1: Update user profile updateUserProfile(conn, 1, "[email protected]"); savepoint1 = conn.setSavepoint("AFTER_PROFILE_UPDATE"); // Step 2: Update preferences (might fail) updateUserPreferences(conn, 1, "theme", "dark"); // Step 3: Log activity logUserActivity(conn, 1, "PROFILE_UPDATE"); conn.commit(); } catch (SQLException e) { if (savepoint1 != null) { // Rollback to savepoint, keeping profile update conn.rollback(savepoint1); // Commit the successful part conn.commit(); System.out.println("Partial operation completed"); } else { conn.rollback(); System.out.println("Operation fully rolled back"); } throw e; } } } private void updateUserProfile(Connection conn, int userId, String email) throws SQLException { // Implementation } private void updateUserPreferences(Connection conn, int userId, String key, String value) throws SQLException { // Implementation } private void logUserActivity(Connection conn, int userId, String activity) throws SQLException { // Implementation } } 2. Transaction Template Pattern
import java.sql.Connection; import java.sql.SQLException; import java.util.function.Consumer; import java.util.function.Function; public class TransactionTemplate { private final ConnectionProvider connectionProvider; public TransactionTemplate(ConnectionProvider connectionProvider) { this.connectionProvider = connectionProvider; } // Execute without return value public void execute(Consumer<Connection> action) throws SQLException { execute(conn -> { action.accept(conn); return null; }); } // Execute with return value public <T> T execute(Function<Connection, T> action) throws SQLException { try (Connection conn = connectionProvider.getConnection()) { conn.setAutoCommit(false); try { T result = action.apply(conn); conn.commit(); return result; } catch (SQLException e) { conn.rollback(); throw e; } catch (RuntimeException e) { conn.rollback(); throw e; } catch (Exception e) { conn.rollback(); throw new SQLException("Transaction failed", e); } } } // Execute with isolation level public <T> T executeWithIsolation(Function<Connection, T> action, int isolationLevel) throws SQLException { try (Connection conn = connectionProvider.getConnection()) { int originalIsolation = conn.getTransactionIsolation(); conn.setTransactionIsolation(isolationLevel); conn.setAutoCommit(false); try { T result = action.apply(conn); conn.commit(); return result; } catch (Exception e) { conn.rollback(); throw e instanceof SQLException ? (SQLException) e : new SQLException(e); } finally { conn.setTransactionIsolation(originalIsolation); } } } // Nested transaction support (using savepoints) public <T> T executeNested(Function<Connection, T> action) throws SQLException { Connection conn = connectionProvider.getConnection(); boolean isNewTransaction = conn.getAutoCommit(); if (isNewTransaction) { conn.setAutoCommit(false); } Savepoint savepoint = conn.setSavepoint(); try { T result = action.apply(conn); if (isNewTransaction) { conn.commit(); } return result; } catch (Exception e) { conn.rollback(savepoint); throw e instanceof SQLException ? (SQLException) e : new SQLException(e); } finally { if (isNewTransaction) { conn.setAutoCommit(true); conn.close(); } } } @FunctionalInterface public interface ConnectionProvider { Connection getConnection() throws SQLException; } } // Usage example class TransactionTemplateExample { private final TransactionTemplate transactionTemplate; public TransactionTemplateExample(TransactionTemplate transactionTemplate) { this.transactionTemplate = transactionTemplate; } public void transferFunds(int fromAcc, int toAcc, double amount) throws SQLException { transactionTemplate.execute(conn -> { // Withdraw from source try (var stmt = conn.prepareStatement( "UPDATE accounts SET balance = balance - ? WHERE id = ?")) { stmt.setDouble(1, amount); stmt.setInt(2, fromAcc); stmt.executeUpdate(); } // Deposit to target try (var stmt = conn.prepareStatement( "UPDATE accounts SET balance = balance + ? WHERE id = ?")) { stmt.setDouble(1, amount); stmt.setInt(2, toAcc); stmt.executeUpdate(); } }); } public Double getAccountBalance(int accountId) throws SQLException { return transactionTemplate.execute(conn -> { try (var stmt = conn.prepareStatement( "SELECT balance FROM accounts WHERE id = ?")) { stmt.setInt(1, accountId); var rs = stmt.executeQuery(); return rs.next() ? rs.getDouble("balance") : null; } }); } } Declarative Transaction Demarcation
1. Spring Framework @Transactional
import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.beans.factory.annotation.Autowired; import java.util.List; @Service public class BankService { private final AccountRepository accountRepository; private final TransactionRepository transactionRepository; @Autowired public BankService(AccountRepository accountRepository, TransactionRepository transactionRepository) { this.accountRepository = accountRepository; this.transactionRepository = transactionRepository; } // Basic transaction @Transactional public void transferMoney(Long fromAccountId, Long toAccountId, Double amount) { Account fromAccount = accountRepository.findById(fromAccountId) .orElseThrow(() -> new AccountNotFoundException(fromAccountId)); Account toAccount = accountRepository.findById(toAccountId) .orElseThrow(() -> new AccountNotFoundException(toAccountId)); if (fromAccount.getBalance() < amount) { throw new InsufficientFundsException("Insufficient funds"); } fromAccount.setBalance(fromAccount.getBalance() - amount); toAccount.setBalance(toAccount.getBalance() + amount); accountRepository.save(fromAccount); accountRepository.save(toAccount); // Log transaction TransactionLog log = new TransactionLog(fromAccountId, toAccountId, amount); transactionRepository.save(log); } // Transaction with specific isolation level @Transactional(isolation = Isolation.READ_COMMITTED) public Double getAccountBalance(Long accountId) { return accountRepository.findById(accountId) .map(Account::getBalance) .orElseThrow(() -> new AccountNotFoundException(accountId)); } // Transaction with timeout @Transactional(timeout = 30) // 30 seconds timeout public void batchUpdateAccounts(List<Account> accounts) { for (Account account : accounts) { accountRepository.save(account); } } // Read-only transaction @Transactional(readOnly = true) public List<TransactionLog> getTransactionHistory(Long accountId) { return transactionRepository.findByAccountId(accountId); } // Transaction with specific rollback rules @Transactional(rollbackFor = {InsufficientFundsException.class, AccountNotFoundException.class}) public void processPayment(Payment payment) { // Payment processing logic } // No rollback for specific exception @Transactional(noRollbackFor = BusinessValidationException.class) public void validateAndProcess(Order order) { // Validation and processing } // Propagation behaviors @Transactional(propagation = Propagation.REQUIRED) public void processOrder(Order order) { // Process order main logic processPayment(order.getPayment()); updateInventory(order.getItems()); } @Transactional(propagation = Propagation.REQUIRES_NEW) public void processPayment(Payment payment) { // Payment processing in separate transaction } @Transactional(propagation = Propagation.NESTED) public void updateInventory(List<OrderItem> items) { // Inventory update that can rollback independently } } // Custom exceptions class AccountNotFoundException extends RuntimeException { public AccountNotFoundException(Long accountId) { super("Account not found: " + accountId); } } class InsufficientFundsException extends RuntimeException { public InsufficientFundsException(String message) { super(message); } } class BusinessValidationException extends RuntimeException { public BusinessValidationException(String message) { super(message); } } 2. Spring Transaction Configuration
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import javax.persistence.EntityManagerFactory; @Configuration @EnableTransactionManagement public class TransactionConfig { @Bean public PlatformTransactionManager transactionManager(EntityManagerFactory emf) { JpaTransactionManager transactionManager = new JpaTransactionManager(); transactionManager.setEntityManagerFactory(emf); return transactionManager; } } // XML Configuration alternative /* <beans> <tx:annotation-driven transaction-manager="transactionManager"/> <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="entityManagerFactory" ref="entityManagerFactory"/> </bean> </beans> */ JTA (Java Transaction API) for Distributed Transactions
1. Programmatic JTA Transactions
import javax.transaction.*; import javax.naming.InitialContext; import javax.naming.NamingException; import java.sql.Connection; import java.sql.SQLException; public class JtaTransactionExample { private UserTransaction userTransaction; private DataSource dataSource; public JtaTransactionExample() throws NamingException { InitialContext ctx = new InitialContext(); this.userTransaction = (UserTransaction) ctx.lookup("java:comp/UserTransaction"); this.dataSource = (DataSource) ctx.lookup("java:comp/env/jdbc/MyDataSource"); } public void transferBetweenDatabases(String fromDb, String toDb, double amount) throws Exception { userTransaction.begin(); Connection conn1 = null; Connection conn2 = null; try { // First database operation conn1 = dataSource.getConnection(); withdrawFromDatabase(conn1, fromDb, amount); // Second database operation conn2 = dataSource.getConnection(); depositToDatabase(conn2, toDb, amount); userTransaction.commit(); System.out.println("Distributed transaction completed"); } catch (Exception e) { userTransaction.rollback(); System.out.println("Distributed transaction rolled back"); throw e; } finally { if (conn1 != null) conn1.close(); if (conn2 != null) conn2.close(); } } private void withdrawFromDatabase(Connection conn, String account, double amount) throws SQLException { // Implementation } private void depositToDatabase(Connection conn, String account, double amount) throws SQLException { // Implementation } // JTA with multiple resources public void processOrderWithMultipleSystems(Order order) throws Exception { userTransaction.begin(); try { // Update database updateOrderInDatabase(order); // Send JMS message sendOrderMessage(order); // Call external service notifyShippingService(order); userTransaction.commit(); } catch (Exception e) { userTransaction.rollback(); throw new TransactionException("Order processing failed", e); } } private void updateOrderInDatabase(Order order) { // Database update } private void sendOrderMessage(Order order) { // JMS message sending } private void notifyShippingService(Order order) { // External service call } } class TransactionException extends Exception { public TransactionException(String message, Throwable cause) { super(message, cause); } } 2. Spring JTA Configuration
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.transaction.jta.JtaTransactionManager; import javax.transaction.TransactionManager; @Configuration @EnableTransactionManagement public class JtaConfig { @Bean public JtaTransactionManager transactionManager() { return new JtaTransactionManager(); } } Transaction Isolation Levels
1. Isolation Level Examples
import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; @Service public class IsolationLevelExamples { // READ_UNCOMMITTED - Can see uncommitted changes (dirty reads) @Transactional(isolation = Isolation.READ_UNCOMMITTED) public Double getUnverifiedBalance(Long accountId) { // Use for non-critical reads where performance is more important than accuracy return accountRepository.getBalance(accountId); } // READ_COMMITTED - Default in most databases @Transactional(isolation = Isolation.READ_COMMITTED) public void updateAccountWithConsistentRead(Long accountId, Double newBalance) { // Prevents dirty reads but allows non-repeatable reads Account account = accountRepository.findById(accountId).orElseThrow(); account.setBalance(newBalance); accountRepository.save(account); } // REPEATABLE_READ - Consistent reads within transaction @Transactional(isolation = Isolation.REPEATABLE_READ) public void consistentAccountAnalysis(Long accountId) { // Both reads will return the same data even if other transactions modify it Double balance1 = accountRepository.getBalance(accountId); // Some processing... Double balance2 = accountRepository.getBalance(accountId); // balance1 == balance2 guaranteed } // SERIALIZABLE - Highest isolation level @Transactional(isolation = Isolation.SERIALIZABLE) public void criticalFinancialOperation(Long fromAccount, Long toAccount, Double amount) { // Complete isolation - no phantom reads // Use for critical financial operations transferMoney(fromAccount, toAccount, amount); } // Database-specific isolation levels @Transactional(isolation = Isolation.READ_COMMITTED) public void oracleSpecificOperations() { // Oracle uses READ_COMMITTED with versioning } } Transaction Propagation Behaviors
1. Propagation Examples
@Service public class PropagationExamples { private final AccountService accountService; private final AuditService auditService; public PropagationExamples(AccountService accountService, AuditService auditService) { this.accountService = accountService; this.auditService = auditService; } // REQUIRED (Default) - Join existing transaction or create new @Transactional(propagation = Propagation.REQUIRED) public void processOrder(Order order) { // If called within transaction, joins it // If no transaction, creates new one accountService.processPayment(order); auditService.logTransaction(order); } // REQUIRES_NEW - Always create new transaction @Transactional(propagation = Propagation.REQUIRES_NEW) public void auditOperation(AuditEvent event) { // Always runs in separate transaction // Useful for audit logs that should persist even if main transaction fails auditService.saveEvent(event); } // NESTED - Create savepoint within existing transaction @Transactional(propagation = Propagation.NESTED) public void updateInventory(OrderItem item) { // Uses savepoints - can rollback independently // Not supported by all databases inventoryService.updateStock(item); } // MANDATORY - Must be called within existing transaction @Transactional(propagation = Propagation.MANDATORY) public void updateAccountBalance(Account account) { // Throws exception if no transaction exists accountRepository.save(account); } // SUPPORTS - Use transaction if exists, otherwise non-transactional @Transactional(propagation = Propagation.SUPPORTS) public Account getAccount(Long accountId) { // Can run with or without transaction return accountRepository.findById(accountId).orElse(null); } // NOT_SUPPORTED - Suspend existing transaction if exists @Transactional(propagation = Propagation.NOT_SUPPORTED) public void generateReport() { // Always runs without transaction // Useful for read-only operations that don't need transaction overhead reportService.generateAccountReport(); } // NEVER - Must not be called within transaction @Transactional(propagation = Propagation.NEVER) public void validateAccount(Account account) { // Throws exception if transaction exists // Useful for validation that should not be part of transaction validationService.validate(account); } } Best Practices and Patterns
1. Transaction Retry Pattern
import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Retryable; import org.springframework.dao.TransientDataAccessException; @Service public class TransactionRetryService { // Retry on transient failures @Retryable( value = {TransientDataAccessException.class, OptimisticLockingFailureException.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2) ) @Transactional public void processWithRetry(Order order) { // This method will be retried on transient failures processOrder(order); } // Custom retry logic @Transactional public void processWithCustomRetry(Order order) { int maxRetries = 3; int attempt = 0; while (attempt < maxRetries) { try { processOrder(order); break; // Success, exit retry loop } catch (OptimisticLockingFailureException e) { attempt++; if (attempt >= maxRetries) { throw e; } // Wait before retry try { Thread.sleep(100 * attempt); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new RuntimeException("Retry interrupted", ie); } } } } private void processOrder(Order order) { // Order processing logic } } 2. Transaction Timeout Management
@Service public class TimeoutManagementService { // Global timeout configuration @Transactional(timeout = 30) public void processBatch(List<Order> orders) { for (Order order : orders) { processSingleOrder(order); } } // Dynamic timeout based on operation @Transactional public void processWithDynamicTimeout(Order order) { // Set query timeout on individual statements EntityManager em = entityManagerFactory.createEntityManager(); try { Query query = em.createQuery("UPDATE Order o SET o.status = :status WHERE o.id = :id"); query.setParameter("status", OrderStatus.PROCESSING); query.setParameter("id", order.getId()); // Set timeout for this specific query javax.persistence.Query jpaQuery = (javax.persistence.Query) query; jpaQuery.setHint("javax.persistence.query.timeout", 5000); // 5 seconds jpaQuery.executeUpdate(); } finally { em.close(); } } // Monitoring transaction duration @Transactional public void processWithMonitoring(Order order) { long startTime = System.currentTimeMillis(); try { processOrder(order); } finally { long duration = System.currentTimeMillis() - startTime; if (duration > 10000) { // 10 seconds threshold log.warn("Long running transaction: {} ms", duration); } } } } 3. Transaction Boundary Best Practices
@Service public class TransactionBoundaryService { // Keep transactions short @Transactional public void processOrderEfficiently(Order order) { // 1. Load necessary data Order managedOrder = orderRepository.findById(order.getId()).orElseThrow(); Customer customer = customerRepository.findById(managedOrder.getCustomerId()).orElseThrow(); // 2. Perform business logic (outside transaction if possible) OrderValidationResult validation = validateOrder(managedOrder, customer); // 3. Perform database operations if (validation.isValid()) { managedOrder.setStatus(OrderStatus.PROCESSED); orderRepository.save(managedOrder); inventoryService.updateStock(managedOrder.getItems()); } // Transaction ends here - keep it short! } // Avoid long-running operations in transactions @Transactional public void processOrderWithExternalCalls(Order order) { // Database operations order.setStatus(OrderStatus.PROCESSING); orderRepository.save(order); // External service call (could be slow) // Consider moving outside transaction or using async shippingService.notifyShipping(order); // More database operations order.setStatus(OrderStatus.SHIPPED); orderRepository.save(order); } // Better approach - split operations public void processOrderOptimized(Order order) { // Phase 1: Database operations in transaction processOrderInTransaction(order); // Phase 2: External calls outside transaction notifyExternalSystems(order); } @Transactional protected void processOrderInTransaction(Order order) { order.setStatus(OrderStatus.PROCESSED); orderRepository.save(order); inventoryService.updateStock(order.getItems()); } protected void notifyExternalSystems(Order order) { // These can fail without affecting database consistency shippingService.notifyShipping(order); notificationService.sendConfirmation(order); } private OrderValidationResult validateOrder(Order order, Customer customer) { // Validation logic that doesn't need database access return new OrderValidationResult(); } } Testing Transactional Code
1. Spring Transaction Testing
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; @ExtendWith(SpringExtension.class) @SpringBootTest @Transactional // Rollback after each test class BankServiceTest { @Autowired private BankService bankService; @Autowired private AccountRepository accountRepository; @Test void testTransferMoney_Success() { // Given Account account1 = new Account(1000.0); Account account2 = new Account(500.0); accountRepository.save(account1); accountRepository.save(account2); // When bankService.transferMoney(account1.getId(), account2.getId(), 200.0); // Then Account updated1 = accountRepository.findById(account1.getId()).orElseThrow(); Account updated2 = accountRepository.findById(account2.getId()).orElseThrow(); assertEquals(800.0, updated1.getBalance()); assertEquals(700.0, updated2.getBalance()); } @Test void testTransferMoney_InsufficientFunds() { // Given Account account1 = new Account(100.0); Account account2 = new Account(500.0); accountRepository.save(account1); accountRepository.save(account2); // When & Then assertThrows(InsufficientFundsException.class, () -> { bankService.transferMoney(account1.getId(), account2.getId(), 200.0); }); // Verify rollback Account afterRollback = accountRepository.findById(account1.getId()).orElseThrow(); assertEquals(100.0, afterRollback.getBalance()); } @Test @Transactional(propagation = Propagation.NOT_SUPPORTED) void testTransactionBoundaries() { // Test without transaction to verify manual transaction management } } Common Pitfalls and Solutions
- Long Transactions: Keep transactions short and focused
- Transaction Scope: Define clear transaction boundaries
- Exception Handling: Ensure proper rollback on exceptions
- Resource Management: Always close resources in finally blocks
- Isolation Levels: Choose appropriate isolation levels for use cases
- Deadlocks: Implement retry logic for deadlock scenarios
- Connection Leaks: Use connection pooling and proper resource cleanup
Proper transaction demarcation is essential for maintaining data consistency and application reliability in enterprise systems.