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

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.
| Characteristic | Meaning | Example |
|---|---|---|
| Atomicity | All success or all failure | If transfer fails, all operations are canceled |
| Consistency | Always adheres to rules | Total amount always constant (no money created or lost) |
| Isolation | Concurrent execution doesn't affect each other | No conflict if A and B transfer simultaneously |
| Durability | Completed results are permanently saved | Results 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();
}- Start transaction: A transaction begins by creating (acquiring) a
Connectionand settingauto_commit=false. This is because if auto_commit is true, each query is immediately reflected in the database. - Execute business logic: Deduct 3,000 KRW from my account and add 3,000 KRW to my friend's account.
- Commit on success: The
commitcommandpermanentlysaves (Durability) all changes made within the transaction. This ensures "all success." - Rollback on failure: The
rollbackoperation cancels the transaction or reverts the data to its previous state. This ensures "all failure." - Remaining cleanup:
- Revert
auto_commitvalue to its previous state. - Return the
Connectionto the Pool.
- Revert
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
- Code Duplication: try-catch-finally in every method
- Business Logic Obscured: Transaction code makes it hard to grasp the core logic
- Prone to Errors: Easy to forget commit/rollback
- 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:
- Same interface: Proxy and real object implement the same interface
- Transparency: The client doesn't know if it's a proxy or a real object
- Controllability: Logic can be added before/after the real object call
Proxy Pattern Use Cases
| Type | Purpose | Example |
|---|---|---|
| Protection Proxy | Access control | Call real object after permission check |
| Virtual Proxy | Lazy loading | Create object only when actually needed |
| Logging Proxy | Log recording | Log before and after method calls |
| Transaction Proxy | Transaction 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:
- Separate into a distinct service (recommended)
- 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 logicto 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
| Term | Meaning | Spring Transaction Example |
|---|---|---|
| Aspect | Module for cross-cutting concerns | Transaction management |
| Join Point | Point where an aspect can be applied | Method execution point |
| Advice | Actual code for the cross-cutting concern | begin/commit/rollback |
| Pointcut | Location where advice is applied | Methods annotated with @Transactional |
| Weaving | Process of applying aspects | Proxy 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:
- Transaction management = cross-cutting concern (Aspect)
- Needed by all service methods
- Always the same pattern: begin -> logic -> commit/rollback
- Implemented with Proxy (Weaving)
- Create a proxy that wraps the real object
- The proxy handles transaction processing
- @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
- 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!- Only supports method execution points
// ā
Supported: before/after method calls
@Transactional
public void method() { }
// ā Not supported: field access, constructor calls, etc.- The
@Transactionalannotation can be declared on classes and methods.
- 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!
