Profile image
Jinyoung
Dev

TIL-01: How to Handle Transactions in Spring Framework - 2

TIL-01: How to Handle Transactions in Spring Framework - 2
0 views
17 min read

In this article, continuing from the previous post, we will delve more specifically into how Spring Framework handles transactions.

Part 4: Spring Framework Transaction

1. Declarative Transaction Management

Declarative vs. Programmatic Approach

Spring provides two ways to manage transactions:

ApproachDescriptionCode Style
ProgrammaticDirectly control transactions with codetransactionManager.begin(), transactionManager.commit()
DeclarativeDeclare transactions with annotations@Transactional

Problems with Programmatic Approach

// ❌ Programmatic Approach - Complex code
@Service
public class OrderService {
  private final TransactionTemplate transactionTemplate;

  public void createOrder(Order order) {
    transactionTemplate.execute(status -> {
      try {
        // Business logic
        orderRepository.save(order);
        paymentRepository.save(payment);
        return null;
      } catch (Exception e) {
        status.setRollbackOnly();
        throw e;
      }
    });
  }
}
  • Problems:
    • πŸ”΄ Transaction code and business logic are mixed
    • πŸ”΄ Repetitive code for every method
    • πŸ”΄ Difficult to test
    • πŸ”΄ Reduced readability

Advantages of Declarative Approach 🌟

// βœ… Declarative Approach - Clean!
@Service
public class OrderService {
  @Transactional
  public void createOrder(Order order) {
    // Business logic only!!
  }
}
  • Advantages:
    • βœ… Separation of business logic and transactions
    • βœ… Elimination of code duplication
    • βœ… Easy to test
    • βœ… Improved readability
    • βœ… Easy to maintain

Core Components of Declarative Transactions

Declarative Transaction Management
    β”‚
    β”‚--- 1. @Transactional (Declaration)
    β”‚    "This method needs a transaction!"
    β”‚
    β”‚--- 2. AOP Proxy (Execution)
    β”‚    Proxy executes transaction logic on behalf
    β”‚
    β”‚--- 3. Transaction Manager (Management)
    β”‚    Actually starts/commits/rolls back transactions
    β”‚
    β”‚--- 4. Transaction Synchronization Manager (Synchronization)
        Stores transaction information in ThreadLocal
  • These four core components are very important:
    • @Transactional
    • AOP Proxy
    • Transaction Manager
    • Transaction Synchronization Manager

We've already looked at AOP Proxy, so let's examine the remaining three concepts in detail in this article.

2. How @Transactional Works

What is @Transactional? 🎯

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
  // Transaction attributes...
}
  • In a nutshell: @Transactional is a marker annotation that instructs Spring to execute this method within a transaction.
  • It's even better if you directly examine the Transactional.java code in the Github spring-framework repository and read it (including comments).

Key Attributes of @Transactional

@Service
public class OrderService {
  @Transactional(
    propagation     = Propagation.REQUIRED,   // Propagation behavior
    isolation       = Isolation.DEFAULT,      // Isolation level
    timeout         = 30,                     // Timeout (seconds)
    readOnly        = false,                  // Read-only status
    rollbackFor     = Exception.class,        // Exceptions to roll back for
    noRollbackFor   = RuntimeException.class  // Exceptions not to roll back for
  )
  public void createOrder(Order order) {
    orderRepository.save(order);
  }
}

1️⃣ Propagation Behavior - Most Important! ⭐️

Problem situation:

@Transactional
public void methodA() {
  // Start Transaction A

  methodB(); // methodB is also @Transactional?

  // Should I create a new transaction? Or use the existing one?
}

@Transactional
public void methodB() {
  // ???
}

Propagation Behavior Types:

AttributeMeaningBehavior
REQUIRED (default)Transaction requiredParticipates if existing, creates new if not
REQUIRES_NEWAlways new transactionSuspends existing and creates new, even if existing
SUPPORTSTransaction supportedParticipates if existing, executes without if not
MANDATORYTransaction mandatoryParticipates if existing, throws exception if not
NOT_SUPPORTEDTransaction not neededSuspends existing, executes without transaction
NEVERTransaction forbiddenThrows exception if existing
NESTEDNested transactionNested within an existing transaction

Most commonly used REQUIRED example:

@Service
public class OrderService {
  @Autowired
  private PaymentService paymentService;

  @Transactional // REQUIRED (default)
  public void createOrder(Order order) {
    // Start Transaction A
    orderRepository.save(order);

    // paymentService.pay() is also @Transactional(REQUIRED)
    // Participates in Transaction A! (does not create a new one)
    paymentService.pay(order.getId());

    // Both execute within Transaction A
    // If one fails, both roll back!
  }
}

@Service
public class PaymentService {
  @Transactional // REQUIRED
  public void pay(Long orderId) {
    paymentRepository.save(payment);
  }
}
  • If an error occurs during the execution of the PaymentService.pay method, not only the pay method but also the data processing operations performed during the createOrder method execution are entirely rolled back.
    • This is because all operations were handled as a single transaction.

REQUIRES_NEW example:

@Service
public class OrderService {
  @Autowired
  private LogService logService;

  @Transactional // REQUIRED
  public void createOrder(Order order) {
    // Transaction A
    orderRepository.save(order);

    // REQUIRES_NEW -> Creates new Transaction B
    logService.log("Order created");

    // Even if the order fails and rolls back, the log is committed because it's a separate transaction!
    throw new RuntimeException("Order failed");
  }
}

@Service
public class LogService {
  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public void log(String message) {
    // Transaction B (independent)
    logRepository.save(log);
    // This is always committed!
  }
}
  • Even if an error occurs in the createOrder method after the LogService.log method finishes executing, the data processing operations performed during the log method execution are not rolled back.
    • This is because they were processed as separate, independent transactions.
    • By creating intentionally separated transactions in this way, you can ensure that data processing operations are always saved, regardless of the success or failure of the original operation.

2️⃣ Isolation (Isolation Level)

Problem: Concurrency Issue

Transaction A                 Transaction B
   β”‚                            β”‚
   β”œβ”€ Check account (10,000)    β”‚
   β”‚                            β”œβ”€ Check account (10,000)
   β”œβ”€ Withdraw 5,000            β”‚
   β”œβ”€ Commit (5,000)            β”‚
   β”‚                            β”œβ”€ Withdraw 3,000
   β”‚                            └─ Commit (???)

Final balance: 2,000? 7,000? πŸ€”
  • This is a case where a 5,000 won withdrawal and a 3,000 won withdrawal operation occur simultaneously from a single account.
  • Since Transaction A and Transaction B each read the original amount (10,000 won) almost simultaneously and then individually perform the deduction operation, the final amount will be 7,000 won when both transactions are complete.
  • This differs from the correct amount of 2,000 won, indicating a concurrency issue.

The isolation attribute of the @Transactional annotation allows specifying various isolation levels to handle this concurrency issue. This article does not delve deeply into the concept of isolation. Isolation is a very important concept in databases and will be covered in detail in a separate article. For now, let's just understand that developers can declaratively specify the level of this isolation.


Isolation Levels:

LevelDescriptionPossible Problems
DEFAULTUses DB defaultVaries per DB
READ_UNCOMMITTEDReads uncommitted dataDirty Read
READ_COMMITTEDReads only committed dataNon-Repeatable Read
REPEATABLE_READRepeatedly reads same dataPhantom Read
SERIALIZABLEFull isolationPerformance degradation

3️⃣ readOnly

Read-Only Optimization:

@Transactional(readOnly = true)
public List<Order> findOrders() {
  // Reads only
  return orderRepository.findAll();
}
  • Effects
    • βœ… JPA: Improved performance by not flushing
    • βœ… JDBC: DB optimized for read-only operations
    • βœ… Prevents accidental data modification

4️⃣ rollbackFor / noRollbackFor

Default Rollback Rules:

  • βœ… RuntimeException (Unchecked): Rollback
  • ❌ Exception (Checked): Commit
// Default behavior
@Transactional
public void method1() {
    // RuntimeException β†’ Rollback βœ…
    throw new RuntimeException("Rolled back");
}

@Transactional
public void method2() throws Exception {
    // Checked Exception β†’ Commit ❌
    throw new Exception("Committed?!");
}

// Custom setting
@Transactional(rollbackFor = Exception.class)
public void method3() throws Exception {
    // Now Checked Exception also rolls back! βœ…
    throw new Exception("Rolled back");
}
  • The basic concept of a transaction is to roll back all data processing operations if an error occurs during method execution. The rollbackFor and noRollbackFor attributes allow fine-grained specification of what constitutes an "error."

3. Role of Transaction Manager

What is a Transaction Manager?

A Transaction Manager is a core component that actually starts, commits, and rolls back transactions.

PlatformTransactionManager Interface πŸ”‘

All Spring Transaction Managers implement this interface. View on Github

Starting from Spring 6.x, the TransactionManager interface was added at a higher level, and PlatformTransactionManager was modified to extend it.

// Let's understand through simplified code!
public interface PlatformTransactionManager {
  // Start transaction
  TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;

  // Commit transaction
  void commit(TransactionStatus status) throws TransactionException;
  
  // Rollback transaction
  void rollback(TransactionStatus status) throws TransactionException;
}
  • 3 Core Methods:
    1. getTransaction(): Starts a new transaction or participates in an existing one
    2. commit(): Commits the transaction
    3. rollback(): Rolls back the transaction

Transaction Manager Implementations πŸ“Š

PlatformTransactionManager (Interface)
    β”‚
    β”œβ”€β”€β”€ DataSourceTransactionManager
    β”‚    └─ Used with JDBC, MyBatis
    β”‚
    β”œβ”€β”€β”€ JpaTransactionManager
    β”‚    └─ Used with JPA, Hibernate
    β”‚
    β”œβ”€β”€β”€ HibernateTransactionManager
    β”‚    └─ Used with Hibernate only
    β”‚
    β”œβ”€β”€β”€ JtaTransactionManager
    β”‚    └─ Used with distributed transactions (JTA)
    β”‚
    └─── WebLogicJtaTransactionManager
        └─ Used with JTA on WebLogic servers

🌟 DataSourceTransactionManager

@Configuration
public class AppConfig {
  @Bean
  public DataSource dataSource() {
    // DB Connection Pool configuration
    return new HikariDataSource();
  }

  @Bean
  public PlatformTransactionManager transactionManager() {
    return  new DataSourceTransactionManager(dataSource());
  }
}

How it works:

// Internal workings (simplified)
public class DataSourceTransactionManager implements PlatformTransactionManager {
  private DataSource dataSource;

  @Override
  public TransactionStatus getTransaction(TransactionDefinition def) {
    // 1. Acquire Connection from DataSource
    Connection conn = dataSource.getConnection();

    // 2. Turn off auto-commit
    conn.setAutoCommit(false);

    // 3. Set isolation level
    conn.setTransactionIsolation(def.getIsolationLevel().value());

    // 4. Store Connection in ThreadLocal
    TransactionSynchronizationManager.bindResource(dataSource, conn);

    return new DefaultTransactionStatus(conn, ...);
  }

  @Override
  public void commit(TransactionStatus status) {
    Connection conn = status.getConnection();
    conn.commit(); // JDBC commit
    conn.close();
  }

  @Override
  public void rollback(TransactionStatus status) {
    Connection conn = status.getConnection();
    conn.rollback(); // JDBC rollback
    conn.close();
  }
}

🌟 JpaTransactionManager

Used with JPA, Hibernate:

@Configuration
@EnableJpaRepositories
public class JpaConfig {
  @Bean
  public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
    // JPA EntityManager configuration
    return new LocalContainerEntityManagerFactoryBean();
  }

  @Bean
  public PlatformTransactionManager transactionManager() {
    return new JpaTransactionManager(
      entityManagerFactory().getObject()
    );
  }
}

How it works:

// Internal workings (simplified)
public class JpaTransactionManager implements PlatformTransactionManager {
  private EntityManagerFactory emf;

  @Override
  public TransactionStatus getTransaction(TransactionDefinition def) {
    // 1. Create EntityManager
    EntityManager em = emf.createEntityManager();

    // 2. Start JPA transaction
    em.getTransaction().begin();

    // 3. Store EntityManager in ThreadLocal
    TransactionSynchronizationManager.bindResource(
      emf, em
    );

    return new DefaultTransactionStatus(em, ...);
  }

  @Override
  public void commit(Transaction status) {
    EntityManager em = status.getEntityManager();
    em.flush();       // Reflect changes to DB
    em.getTransaction().commit(); // JPA commit
    em.close();
  }

  @Override
  public void rollback(TransactionStatus status) {
    EntityManager em = status.getEntityManager();
    em.getTransaction().rollback(); // JPA rollback
    em.close();
  }
}

πŸ“Š DataSource vs. JPA Transaction Manager Comparison

ItemDataSourceTransactionManagerJpaTransactionManager
Technology UsedJDBC, MyBatisJPA, Hibernate
Managed ResourceConnectionEntityManager
AcquisitiondataSource.getConnection()emf.createEntityManager()
Startconn.setAutoCommit(false)em.getTransaction().begin()
Commitconn.commit()em.getTransaction().commit()
Rollbackconn.rollback()em.getTransaction().rollback()
Closeconn.close()em.close()
  • JpaTransactionManager also supports DataSource. JDBC code using DataSource can also be included in the same transaction.

πŸ”ƒ Key Roles of Transaction Manager

3 Main Responsibilities of Transaction Manager:

1. Transaction Resource Management
   β”œβ”€ Acquire Connection or EntityManager
   └─ Store in ThreadLocal (thread-safe)

2. Transaction Boundary Setting
   β”œβ”€ Start: begin / setAutoCommit(false)
   β”œβ”€ End: commit / rollback
   └─ Apply propagation behavior (REQUIRED, REQUIRES_NEW, etc.)

3. Exception Translation
   └─ Translate DB exceptions to Spring exceptions

4. Transaction Synchronization Manager

ThreadLocal-based Transaction Synchronization:

// Simplified code
public abstract class TransactionSynchronizationManager {

  // Store transaction resources with ThreadLocal
  private static final ThreadLocal<Map<Object, Object>> resources = 
    new NamedThreadLocal<>("Transactional resources");

  // Store current thread's connection
  public static void bindResource(Object key, Object value) {
    Map<Object, Object> map = resources.get();
    if (map == null) {
      map = new HashMap<>();
      resource.set(map);
    }
    map.put(key, value);
  }

  // Retrieve current thread's connection
  public static Object getResource(Object key) {
    Map<Object, Object> map = resources.get();
    return map != null ? map.get(key) : null;
  }
}

Why use ThreadLocal?

@Service
public class OrderService {
  @Autowired
  private OrderRepository orderRepository;
  @Autowired
  private PaymentRepository paymentRepository;
  
  @Transactional
  public void createOrder(Order order) {
    // 1. Transaction Manager acquires Connection
    // 2. Stores it in ThreadLocal
    
    orderRepository.save(order);
    // β†’ Repository retrieves the same Connection from ThreadLocal
    
    paymentRepository.save(payment);
    // β†’ This also retrieves the same Connection from ThreadLocal
    
    // Same Connection = Same Transaction!
  }
}
  • Effects:
    • βœ… All Repositories within the same thread use the same Connection
    • βœ… No need to pass Connection as a parameter

So far, we've learned about the individual components that make up a transaction. Now, let's look at the overall flow to get the big picture.

Full Flow of Transaction Start/Commit/Rollback

🎯 Example Scenario:

@RestController
@RequiredArgsConstructor
public class OrderController {
  private final OrderService orderService; // proxy injected

  @PostMapping("/orders")
  public ResponseEntity<Order> createOrder(@RequestBody OrderRequest request) {
    orderService.createOrder(request.toOrder());
    return ResponseEntity.ok().build();
  }
}

@Service
@RequiredArgsConstructor
public class OrderService {
  private final OrderRepository orderRepository;
  private final PaymentService paymentService; // proxy injected

  @Transactional
  public void createOrder(Order order) {
    // 1. Save order
    orderRepository.save(order);

    // 2. Process payment (calls another @Transactional method)
    paymentService.processPayment(order.getId());

    // 3. Decrease stock
    order.decreaseStock();
  }
}

@Service
@RequiredArgsConstructor
public class PaymentService {
  private final PaymentRepository paymentRepository;

  @Transactional(propagation = Propagation.REQUIRED)
  public void processPayment(Long orderId) {
    Payment payment = new Payment(orderId);
    paymentRepository.save(payment);
  }
}

Overall Call Flow

[1] HTTP Request
     ⬇️
[2] OrderController.createOrder()
     ⬇️
[3] OrderService (Proxy).createOrder() ⬅️ Enters Proxy!
     ⬇️
[4] TransactionInterceptor.invoke()
     ⬇️
[5] TransactionAspectSupport.invokeWithinTransaction()
     ⬇️
[6] TransactionManager.getTransaction() ⬅️ Transaction Start
     ⬇️
[7] OrderService (Original).createOrder() ⬅️ Actual method execution
     ⬇️
[8] orderRepository.save()
     ⬇️
[9] PaymentService (Proxy).processPayment() ⬅️ Nested call
     ⬇️
[10] Participates in existing transaction (REQUIRED)
     ⬇️
[11] paymentRepository.save()
     ⬇️
[12] Method completes
     ⬇️
[13] TransactionManager.commit() ⬅️ Transaction Commit

Detailed Analysis by Phase

Let's now examine the entire process from an HTTP request to transaction commit, step by step. We will explain based on the example code. This example code is a simplified version of actual Spring Framework code, written for better understanding.

Phase 1: Entering the Proxy

// [3] Proxy method call
orderService.createOrder(order);

// What actually happens:
OrderService$$EnhancerBySpringCGLIB$$123412344.createOrder(order) {
  // Create MethodInvocation
  MethodInvocation invocation = createMethodInvocation(
    "createOrder",
    new Object[]{order}
  );

  // [4] Delegate to TransactionInterceptor
  return interceptor.invoke(invocation);
}

Phase 2: TransactionInterceptor Execution

// [4] TransactionInterceptor.invoke()
public class TransactionInterceptor extends TransactionAspectSupport {
  @Override
  public Object invoke(MethodInvocation invocation) throws Throwable {
    // 1. Target class information
    Class<?> targetClass = invocation.getThis().getClass();

    // 2. Method to be called
    Method method = invocation.getMethod();

    // [5] Execute method within a transaction
    return invokeWithinTransaction(
      method,
      targetClass,
      invocation::proceed
    )
  }
}
  • For the actual class code, please refer here.

Phase 3: Parsing Transaction Attributes

// [5] TransactionAspectSupport.invokeWithinTransaction()
protected Object invokeWithinTransaction(
  Method method,
  Class<?> targetClass,
  InvocationCallback invocation
) throws Throwable {
  // 1. Read @Transactional attributes
  TransactionAttribute txAttr = getTransactionAttributeSource()
    .getTransactionAttribute(method, targetClass);

  // txAttr contents:
  //  - propagation: REQUIRED
  //  - isolation: DEFAULT
  //  - timeout: -1 (unlimited)
  //  - readOnly: false
  //  - rollbackFor: [RuntimeException.class]

  // 2. Find TransactionManager
  PlatformTransactionManager tm = determineTransactionManager(txAttr);

  // 3. Execute within a transaction
  String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
  return createTransactionIfNecessary(tm, txAttr, joinpointIdentification)
    .execute(status -> {
      return invocation.proceedWithInvocation();
    });
}
  • For the actual implementation code, please refer to this link.

Phase 4: Starting the Transaction

// [6] TransactionManager.getTransaction()
public class DataSourceTransactionManager {
  @Override
  public TransactionStatus getTransaction(TransactionDefinition definition) {
    // 1. Check if an existing transaction exists
    Object transaction = doGetTransaction();

    // 2. Check existing transaction
    if (isExistingTransaction(transaction)) {
      // Handle based on propagation behavior
      return handleExistingTransaction(definition, transaction);
    }

    // 3. A new transaction needs to be started
    return startNewTransaction(definition, transaction);
  }

  private TransactionStatus startNewTransaction(
    TransactionDefinition definition,
    Object transaction
  ) {
    // 1. Acquire Connection
    Connection conn = dataSource.getConnection();

    // 2. Turn off auto-commit
    conn.setAutoCommit(false);

    // 3. Set isolation level
    if (definition.getIsolationLevel() != Isolation.DEFAULT) {
      conn.setTransactionIsolation(
        definition.getIsolationLevel().value()
      );
    }

    // 4. Store Connection in ThreadLocal
    TransactionSynchronizationManager.bindResource(
      dataSource,
      new ConnectionHolder(conn)
    );

    // 5. Return TransactionStatus
    return new DefaultTransactionStatus(
      transaction,
      true,   // newTransaction = true
      conn,
      definition
    );
  }
}

Current State:

Thread: http-nio-8080-exec-1
  β”‚
  β”œβ”€ ThreadLocal<Map<DataSource, ConnectionHolder>>
  β”‚   └─ HikariDataSource β†’ ConnectionHolder
  β”‚                          └─ Connection (autoCommit=false) βœ…
  β”‚
  └─ TransactionStatus
      β”œβ”€ transaction: DataSourceTransaction
      β”œβ”€ newTransaction: true
      β”œβ”€ connection: HikariProxyConnection@123abc
      └─ completed: false

Phase 5: Actual Method Execution

// [7] Execute original method
orderService.createOrder(order);

public void createOrder(Order order) {
  orderRepository.save(order);
  // [8] orderRepository.save()
    // Retrieve Connection within Repository:
    Connection conn = TransactionSynchronizationManager.getResource(dataSource);
    // Execute SQL with the same Connection
    PreparedStatement stmt = conn.prepareStatement(
      "INSERT INTO orders (id, amount) VALUES (?, ?)"
    );
    stmt.setLong(1, order.getId());
    stmt.setInt(2, order.getAmount());
    stmt.executeUpdate(); // Not yet committed!

  // [9] Call PaymentService
  paymentService.processPayment(order.getId());
}

Phase 6: Handling Nested Transaction (REQUIRED)

// [9] Call PaymentService (Proxy).processPayment()
// ➑️ TransactionInterceptor executes again

public TransactionStatus getTransaction(TransactionDefinition definition) {
  // 1. Check for existing transaction
  ConnectionHolder holder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);

  if (holder != null && holder.isTransactionActive()) {
    // Existing transaction found βœ…

    // 2. Check REQUIRED attribute
    if (definition.getPropagationBehavior() == PROPAGATION_REQUIRED) {
      // Participate in existing transaction
      return new DefaultTransactionStatus(
        transaction,
        false,    // newTransaction = false (not a new transaction!)
        holder.getConnection(),
        definition
      )
    }
  }
}

Important Point:

OrderService.createOrder() [Transaction A]
          ⬇️
orderRepository.save()  [Uses Transaction A]
          ⬇️
PaymentService.processPayment() [Participates in Transaction A] ⭐️
          ⬇️
paymentRepository.save()  [Uses Transaction A]
          ⬇️
All use the same Connection, same transaction!
If even one fails, everything rolls back!

Phase 7: Method Completion and Commit

// [12] createOrder() method completes successfully
// ➑️ Returns to TransactionInterceptor

protected Object invokewithinTransaction(...) {
  try {
    // Execute method
    Object result = invocation.proceedWithInvocation();

    // [13] Commit on success
    commitTransactionAfterReturning(txInfo);

    return result;
  } catch (Throwable ex) {
    // Rollback on failure
    completeTransactionAfterThrowing(txInfo, ex);
    throw ex;
  }
}

// [13] Execute commit
private void commitTransactionAfterReturning(TransactionInfo txInfo) {
  if (txInfo.getTransactionStatus().inNewTransaction()) {
    // If this method started the transaction, commit
    txInfo().getTransactionManager().commit(txInfo.getTransactionStatus());
  }
  // If participated (REQUIRED) ➑️ do not commit
}

// TransactionManager.commit()
public void commit(TransactionStatus status) {
  if (status.isCompleted()) {
    throw new IllegalTransactionStateException("Transaction already completed");
  }

  DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;

  // Check for rollback mark
  if (defStatus.isLocalRollbackOnly()) {
    rollback(status);
    return;
  }

  // Actual commit
  processCommit(defStatus);
}

private void processCommit(DefaultTransactionStatus status) {
  try {
    // 1. Call Connection.commit()
    Connection conn = status.getConnection();
    conn.commit(); βœ…

    // 2. Call TransactionSynchronization callbacks
    triggerAfterCommit();
  } finally {
    // 3. Clean up resources
    cleanupAfterCompletion(status);
  }
}

private void cleanupAfterCompletion(TransactionStatus status) {
  // 1. Remove Connection from ThreadLocal
  TransactionSynchronizationManager.unbindResource(dataSource);

  // 2. Return Connection (➑️ to Connection Pool)
  Connection conn = status.getConnection();
  DataSourceUtils.releaseConnection(conn, dataSource);
}

Final State:

Thread: http-nio-8080-exec-1
    β”‚
    β”œβ”€ ThreadLocal<Map> β†’ (empty, cleaned up) βœ…
    β”‚
    └─ DB Status
        β”œβ”€ orders table: INSERT committed βœ…
        └─ payments table: INSERT committed βœ…

The scenario we just examined involves two different operationsβ€”adding data to orders and payments tablesβ€”being processed within a single, common transaction.

We saw that all operations were successfully completed, and a commit occurred at the final stage.

Transaction Rollback Scenario

Now, let's look at the process of a rollback occurring during transaction processing.

πŸ”΄ Exception Case

@Transactional
public void createOrder(Order order) {
  orderRepository.save(order);

  // Exception occurs! πŸ’£
  if (order.getAmount() > 10000) {
    throw new RuntimeException("Amount exceeded");
  }

  paymentService.processPayment(order.getId());
}

Rollback Flow

protected Object invokeWithinTransaction(...) {
  try {
    Object result = invocation.proceedWithInvocation(); // Exception occurs!
    commitTransactionAfterReturning(txInfo);
    return result;
  } catch (Throwable ex) {
    // [Catch exception!]
    completeTransactionAfterThrowing(txInfo, ex);
    throw ex;
  }
}

private void completeTransactionAfterThrowing(
  TransactionInfo txiInfo, Throwable ex
) {
  // 1. Check if it's an exception that should trigger a rollback
  if (txInfo.transactionAttribute.rollbackOn(ex)) {
    // RuntimeException -> true (default)
    // Exception -> false (default)

    // 2. Execute rollback
    txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
  } else {
    // Commit without rolling back
    txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
  }
}

// TransactionManager.rollback()
public void rollback(TransactionStatus status) {
  DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
  processRollback(defStatus);
}

private void processRollback(DefaultTransactionStatus status) {
  try {
    // 1. Call Connection.rollback()
    Connection conn = status.getConnection();
    conn.rollback(); πŸ”„

    // 2. TransactionSynchronization callbacks
    triggerAfterCompletion(STATUS_ROLLED_BACK);
  } finally {
    // 3. Clean up resources
    cleanupAfterCompletion(status);
  }
}

Result:

DB Status:
β”œβ”€ orders table: No changes (rolled back) πŸ”„
└─ payments table: No changes (not even executed)

Core Component Relationship Diagram

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚          Client (Controller)                β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚ Calls
                  ⬇️
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         Proxy (CGLIB generated)             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚   TransactionInterceptor              β”‚  β”‚
β”‚  β”‚    β”œβ”€ Reads @Transactional attributes β”‚  β”‚
β”‚  β”‚    └─ Calls TransactionManager        β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚
                  ⬇️
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   PlatformTransactionManager                β”‚
β”‚    β”œβ”€ getTransaction() (starts transaction) β”‚
β”‚    β”œβ”€ commit() (commits)                    β”‚
β”‚    └─ rollback() (rolls back)               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚
                  ⬇️
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  TransactionSynchronizationManager          β”‚
β”‚    └─ ThreadLocal<Connection>               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚
                  ⬇️
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   DataSource (Connection Pool)              β”‚
β”‚    └─ Connection (actual DB connection)     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

In this article, we specifically examined how Spring Framework handles transactions.

Transactions are a very important yet complex concept related to databases. Therefore, a proper understanding of this concept is crucial. I hope this article has given you a general grasp of what transactions are, why they need to be handled, and how they are processed.

Thank you for reading to the end!

Comments (0)

Checking login status...

No comments yet. Be the first to comment!