Profile image
Jinyoung
Dev

TIL-01: How to Handle Transactions in Spring Framework - Part 1

TIL-01: How to Handle Transactions in Spring Framework - Part 1
0 views
12 min read

I recently started TIL (Today I Learned). It involves selecting a specific topic each day, studying it, and documenting it separately.

I'm trying this TIL using Claude's 'Projects' feature. I create a dedicated TIL Project, enter a learning topic and a few keywords into the chat, and Claude automatically devises a study plan. In the final part of the learning process, I explain what I learned that day as if I were teaching someone else, and Claude evaluates it. This can be seen as the Feynman Technique.

I've created a separate TIL repository where I continuously upload my learning content.


Part 1: Database Transaction Concepts

What is a Transaction?

A transaction is a mechanism that groups multiple database operations into a single logical unit, guaranteeing either "all success" or "all failure."

Why Transactions Are Necessary

I'll explain why transactions are necessary through a real-life example you might encounter.

Real-life Example: Bank Account Transfer

My Account: 10,000 KRW
Friend's Account: 5,000 KRW

Process of me transferring 3,000 KRW to my friend:
  1. Deduct 3,000 KRW from my account -> 7,000 KRW
  2. Increase friend's account by 3,000 KRW -> 8,000 KRW

What if there were no transactions? 😱

1. Deduction of 3,000 KRW from my account successful āœ…
2. [Server suddenly crashes! šŸ”„]
3. Addition of 3,000 KRW to friend's account fails āŒ

Result:
  - My Account: 7,000 KRW (3,000 KRW disappeared!)
  - Friend's Account: 5,000 KRW (didn't receive it!)
  - Conclusion: 3,000 KRW evaporated. Data consistency is broken.

What if there were transactions? šŸ¤—

Transaction Start
ā”œā”€ 1. Deduct 3,000 KRW from my account
ā”œā”€ 2. [Server crash occurs!]
└─ 3. Add 3,000 KRW to friend's account (not executed)
   → Transaction Rollback
   → All operations canceled
   → Restored to original state

Result:
  - My Account: 10,000 KRW (restored)
  - Friend's Account: 5,000 KRW (original state maintained)
  - Conclusion: Money is safe! Consistency preserved.

Four Characteristics of Transactions (ACID)

Transactions have four characteristics. The acronym ACID is derived from the first letter of each of these characteristics. It's one of the first things learned when explaining transactions. Rather than trying to force memorization of these characteristics, it's important to understand their necessity and consider why such characteristics are indispensable.

CharacteristicMeaningExample
AtomicityAll success or all failureIf transfer fails, all operations are canceled
ConsistencyAlways adheres to rulesTotal amount always constant (no money created or lost)
IsolationConcurrent execution doesn't affect each otherNo conflict if A and B transfer simultaneously
DurabilityCompleted results are permanently savedResults persist even if server crashes after commit

Transaction Lifecycle šŸ”„

1. BEGIN (Start)
        ā¬‡ļø
2. Execute multiple SQL statements
   ā”œā”€ INSERT
   ā”œā”€ UPDATE
   ā”œā”€ DELETE
        ā¬‡ļø
3. Determine success or failure
   ā”œā”€ Success → COMMIT (Confirm) āœ…
   └─ Failure → ROLLBACK (Cancel) āŒ

Transaction Example in Java Code (JDBC):

Connection conn = dataSource.getConnection();

try {
  // 1. Start transaction
  conn.setAutoCommit(false);

  // 2. Execute business logic
  Statement stmt = conn.createStatement();
  stmt.executeUpdate("UPDATE account SET balance = balance - 3000 WHERE id = 1");
  stmt.executeUpdate("UPDATE account set balance = balance + 3000 WHERE id = 2");

  // 3. Commit on success
  conn.commit();
} catch (Exception e) {
  // 4. Rollback on failure
  conn.rollback();
} finally {
  // 5. Remaining cleanup
  conn.setAutoCommit(true);
  conn.close();
}
  1. Start transaction: A transaction begins by creating (acquiring) a Connection and setting auto_commit=false. This is because if auto_commit is true, each query is immediately reflected in the database.
  2. Execute business logic: Deduct 3,000 KRW from my account and add 3,000 KRW to my friend's account.
  3. Commit on success: The commit command permanently saves (Durability) all changes made within the transaction. This ensures "all success."
  4. Rollback on failure: The rollback operation cancels the transaction or reverts the data to its previous state. This ensures "all failure."
  5. Remaining cleanup:
    • Revert auto_commit value to its previous state.
    • Return the Connection to the Pool.

What if code is written without transactions? āš ļø

// āŒ Bad example: No transaction
public void transfer(Long fromId, Long toId, int amount) {
  // Each operation is independently auto-committed
  accountRepository.decrease(fromId, amount); // Auto-commit āœ…

  // What if an exception occurs here?
  throw new RuntimeException("Error!");

  accountRepository.increase(toId, amount); // Not executed! āŒ
  // The first operation is already committed and cannot be rolled back!
}

Problems with Transaction Code Examined So Far

  1. Code Duplication: try-catch-finally in every method
  2. Business Logic Obscured: Transaction code makes it hard to grasp the core logic
  3. Prone to Errors: Easy to forget commit/rollback
  4. Difficult to Test: Need to test transaction code as well
// 😢 Transaction code overwhelms business logic
public void businessLogic() {
  Connection conn = null;
  try {
    conn = dataSource.getConnection();
    conn.setAutoCommit(false);

    // Actual business logic is only 2 lines
    orderService.createOrder();
    paymentService.processPayment();

    conn.commit();
  } catch (Exception e) {
    if (conn != null) {
      try {
        conn.rollback();
      } catch (SQLException ex) {
        // Rollback can also fail!
      }
    }
  } finally {
    if (conn != null) {
      try {
        conn.close();
      } catch (SQLException e) {
        // close also needs exception handling!
      }
    }
  }
}

So far, we've looked at why transactions are necessary, how they are implemented in Java (JDBC) code, and their pros and cons. In the remainder of this article, we'll explore how this transaction handling pattern is implemented in Java code/Spring Framework.


Part 2: Understanding the Proxy Pattern

Before diving straight into the Spring Framework, let's understand the concepts of Proxy Pattern and AOP. This is an essential step to understand how Spring Framework handles transactions.

What is the Proxy Pattern?

A proxy is an agent that stands in for a real object, a design pattern that allows additional operations to be performed in front of the real object.

Why is a Proxy Needed?

Real-life Example: Secretary (Proxy) vs. CEO (Real Object)

Client requests a meeting with the CEO
            ā¬‡ļø
Secretary receives it first (Proxy)
            ā¬‡ļø
   ā”œā”€ Check schedule
   ā”œā”€ Book meeting room
   ā”œā”€ Prepare materials
   └─ Forward to CEO (Real Object)
            ā¬‡ļø
    Meeting conducted
            ā¬‡ļø
    Secretary handles wrap-up (Proxy)
    ā”œā”€ Write meeting minutes
    └─ Follow-up actions
  • Key: The CEO (Real Object) focuses only on their main job, while the secretary (Proxy) handles the auxiliary tasks!

Proxy Pattern Structure

// 1. Interface (Common Contract)
interface Service {
  void execute();
}

// 2. Real Object
class RealService implements Service {
  @Override
  public void execute() {
    System.out.println("Executing core business logic!");
  }
}

// 3. Proxy Object
class ServiceProxy implements Service {
  private RealService realService;

  public ServiceProxy(RealService realService) {
    this.realService = realService;
  }

  @Override
  public void execute() {
    // Additional tasks "before" calling the actual method
    System.out.println("[Proxy] Pre-processing: Logging, security check...");
    
    // Call the actual object's method
    this.realService.execute();

    // Additional tasks "after" calling the actual method
    System.out.println("[Proxy] Post-processing: Resource cleanup...");
  }
}

// 4. Client Usage
Service service = new ServiceProxy(new RealService());
service.execute();

Key Points of Proxy

Client
  ā¬‡ļø
Proxy (Delegate)
  ā”œā”€ Pre-processing
  ā”œā”€ Call Real Object
  └─ Post-processing

Important characteristics:

  1. Same interface: Proxy and real object implement the same interface
  2. Transparency: The client doesn't know if it's a proxy or a real object
  3. Controllability: Logic can be added before/after the real object call

Proxy Pattern Use Cases

TypePurposeExample
Protection ProxyAccess controlCall real object after permission check
Virtual ProxyLazy loadingCreate object only when actually needed
Logging ProxyLog recordingLog before and after method calls
Transaction ProxyTransaction management⭐ What we'll learn here!

Important Constraints of the Proxy Pattern

Spring's proxy only works when called externally.

// Problematic code example
@Service
public class OrderService {
  public void processOrder() {
    // āŒ Calling an internal method of the same class - transaction not applied!
    this.createOrder();
  }

  @Transactional
  public void createOrder() {
    // Transaction not started!
    orderRepository.save(order);
  }
}
  • Reason: this.createOrder() calls the real object directly, bypassing the proxy.
  • Solutions:
    1. Separate into a distinct service (recommended)
    2. Use Self-injection

Connection between Spring and Transactions

How Spring manages proxy transactions:

// Service written by developer (Real Object)
@Service
class OrderService {
  @Transactional // This annotation is magic
  public void createOrder() {
    // Focus only on business logic
    orderRepository.save(order);
    paymentRepository.save(payment);
  }
}

// Proxy automatically generated by Spring
class OrderServiceProxy extends OrderService {
  private TransactionManager txManager;

  @Override
  public void createOrder() {
    // 1. Start transaction
    txManager.begin();

    try {
      // 2. Call the actual method
      super.createOrder();

      // 3. Commit transaction
      txManager.commit();
    } catch (Exception e) {
      // 4. Rollback on exception
      txManager.rollback();
      throw e;
    }
  }
}

šŸ‘‰ Developers only need to attach the @Transactional annotation, and Spring automatically creates a proxy to handle transaction processing for them!


Part 3: AOP Concepts Review

What is AOP?

AOP (Aspect-Oriented Programming) is a programming paradigm that separates cross-cutting concerns from core business logic to eliminate code duplication.

Why is AOP Necessary?

Problem: Adding Logging and Transactions to Every Method

// āŒ Without AOP - Code duplication hell
public void method1() {
  log.info("method1 start");           // Logging (cross-cutting)
  transactionManager.begin();         // Transaction (cross-cutting)
  try {
      // Core business logic
      businessLogic1();
      transactionManager.commit();    // Transaction (cross-cutting)
      log.info("method1 end");       // Logging (cross-cutting)
  } catch (Exception e) {
      transactionManager.rollback();  // Transaction (cross-cutting)
      log.error("method1 failed", e);   // Logging (cross-cutting)
  }
}

public void method2() {
  log.info("method2 start");           // Same code repeated!
  transactionManager.begin();
  try {
      businessLogic2();
      transactionManager.commit();
      log.info("method2 end");
  } catch (Exception e) {
      transactionManager.rollback();
      log.error("method2 failed", e);
  }
}

// method3, method4... endless repetition! 😱

Solved with AOP:

// āœ… With AOP - Clean business logic
@Transactional  // Transaction handled by AOP
@Logging        // Logging also handled by AOP
public void method1() {
  // Only core business logic!
  businessLogic1();
}

@Transactional
@Logging
public void method2() {
  businessLogic2();
}

Key AOP Terminology

TermMeaningSpring Transaction Example
AspectModule for cross-cutting concernsTransaction management
Join PointPoint where an aspect can be appliedMethod execution point
AdviceActual code for the cross-cutting concernbegin/commit/rollback
PointcutLocation where advice is appliedMethods annotated with @Transactional
WeavingProcess of applying aspectsProxy creation

How AOP Works

Code written by developer:
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│  @Transactional   │
│  public void      │
│  createOrder() {  │
│    Business Logic │
│  }                │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
         ā¬‡ļø
AOP automatically transforms
         ā¬‡ļø
Actual code executed:
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ Transaction Start       │ ā¬…ļø Advice (Before)
│       ā¬‡ļø                │
│ Execute Business Logic  │ ā¬…ļø Join Point
│       ā¬‡ļø                │
│ Commit on success       │
│ / Rollback on failure   │ ā¬…ļø Advice (After)
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Proxy Pattern + AOP = Spring Transactions

Spring transactions are the result of combining the Proxy Pattern and AOP. Transaction management, a cross-cutting concern, is defined with AOP and implemented using the Proxy Pattern. If the Proxy Pattern provides the structure of a "secretary," AOP defines "what tasks to assign to the secretary." Spring combines these two to automate transaction management.

3-step connection:

  1. Transaction management = cross-cutting concern (Aspect)
    • Needed by all service methods
    • Always the same pattern: begin -> logic -> commit/rollback
  2. Implemented with Proxy (Weaving)
    • Create a proxy that wraps the real object
    • The proxy handles transaction processing
  3. @Transactional = Specify application location (Pointcut)
    • Mark which methods to apply transactions to

To visualize this structure:

Method annotated with @Transactional
    ā¬‡ļø
AOP detects
    ā¬‡ļø
Proxy created
    ā¬‡ļø
Proxy manages transaction
  ā”œā”€ begin()
  ā”œā”€ Call actual method
  └─ commit() / rollback()

Characteristics of Spring AOP

  1. Runtime Proxy-based
// At compile time, original code remains as is
@Transactional
public void createOrder() {
  orderRepository.save(order);
}

// At runtime, a proxy is created and executed
// Developers don't need to write the proxy directly!
  1. Only supports method execution points
// āœ… Supported: before/after method calls
@Transactional
public void method() { }

// āŒ Not supported: field access, constructor calls, etc.
  • The @Transactional annotation can be declared on classes and methods.
  1. Only applies to Spring Beans
// āœ… Applied: Spring-managed bean
@Service
class OrderService {
    @Transactional
    public void createOrder() { }
}

// āŒ Not applied: object created with 'new'
OrderService service = new OrderService();
service.createOrder(); // No transaction!

Without AOP vs. With AOP

Code Comparison:

// āŒ Without AOP (100 lines)
public class OrderService {
    public void createOrder() {
        // Transaction code: 15 lines
        Connection conn = null;
        try {
            conn = dataSource.getConnection();
            conn.setAutoCommit(false);
            
            // Business logic: 3 lines
            orderRepository.save(order);
            paymentRepository.save(payment);
            emailService.send(email);
            
            conn.commit();
        } catch (Exception e) {
            if (conn != null) conn.rollback();
            throw e;
        } finally {
            if (conn != null) conn.close();
        }
    }
    
    // 5 other methods also repeat the same...
}

// āœ… With AOP (10 lines)
@Service
public class OrderService {
    
    @Transactional
    public void createOrder() {
        // Only business logic: 3 lines
        orderRepository.save(order);
        paymentRepository.save(payment);
        emailService.send(email);
    }
    
    // Other methods are clean too!
}

Benefits:

  • Code duplication removed: 90% reduction
  • Readability improved: Only core logic visible
  • Maintainability enhanced: Only one place to modify transaction policy

So far, we've briefly looked at the concept of transactions and how Spring Framework handles transaction processing with simplified examples. In the next article, we will delve deeper with concrete class examples that Spring uses for transaction processing.

Thank you for reading to the end!

Comments (0)

Checking login status...

No comments yet. Be the first to comment!