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

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:
| Approach | Description | Code Style |
|---|---|---|
| Programmatic | Directly control transactions with code | transactionManager.begin(), transactionManager.commit() |
| Declarative | Declare 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:
@TransactionalAOP ProxyTransaction ManagerTransaction 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:
@Transactionalis 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-frameworkrepository 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:
| Attribute | Meaning | Behavior |
|---|---|---|
| REQUIRED (default) | Transaction required | Participates if existing, creates new if not |
| REQUIRES_NEW | Always new transaction | Suspends existing and creates new, even if existing |
| SUPPORTS | Transaction supported | Participates if existing, executes without if not |
| MANDATORY | Transaction mandatory | Participates if existing, throws exception if not |
| NOT_SUPPORTED | Transaction not needed | Suspends existing, executes without transaction |
| NEVER | Transaction forbidden | Throws exception if existing |
| NESTED | Nested transaction | Nested 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.paymethod, not only thepaymethod but also the data processing operations performed during thecreateOrdermethod 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
createOrdermethod after theLogService.logmethod finishes executing, the data processing operations performed during thelogmethod 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:
| Level | Description | Possible Problems |
|---|---|---|
| DEFAULT | Uses DB default | Varies per DB |
| READ_UNCOMMITTED | Reads uncommitted data | Dirty Read |
| READ_COMMITTED | Reads only committed data | Non-Repeatable Read |
| REPEATABLE_READ | Repeatedly reads same data | Phantom Read |
| SERIALIZABLE | Full isolation | Performance 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
rollbackForandnoRollbackForattributes 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:
getTransaction(): Starts a new transaction or participates in an existing onecommit(): Commits the transactionrollback(): 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
| Item | DataSourceTransactionManager | JpaTransactionManager |
|---|---|---|
| Technology Used | JDBC, MyBatis | JPA, Hibernate |
| Managed Resource | Connection | EntityManager |
| Acquisition | dataSource.getConnection() | emf.createEntityManager() |
| Start | conn.setAutoCommit(false) | em.getTransaction().begin() |
| Commit | conn.commit() | em.getTransaction().commit() |
| Rollback | conn.rollback() | em.getTransaction().rollback() |
| Close | conn.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!
