> For the complete documentation index, see [llms.txt](https://tech.x2bee.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://tech.x2bee.com/dev-guide/developer-guide-en/dev-start/images-and-media/transaction-handling.md).

# Transaction Handling

Transaction handling follows the approach below.

1. **Transaction Management Approach**

* Transactions are explicitly managed via the `@Transactional` annotation (revised May 2023)

2. **Database Structure and Connection Principles**

The X2BEE Framework assumes a dual structure by default, consisting of a ReadWrite database (Master) and a ReadOnly database (Slave).

Connection method provided for development convenience:

* Classes and methods annotated with `@Transactional` connect to the ReadWrite database (Master)
* Service-layer methods without the `@Transactional` annotation connect to the ReadOnly database (Slave) by default

3. **Multiple Data Sources and Transaction Connection (added June 2023)**
   * Supports connecting multiple data sources within a single transaction, using different database schemas and `TransactionManager` instances

***

## Transaction Propagation Levels

* **Propagation.REQUIRED (default)**\
  This is how a method behaves when its transaction is set to `Propagation.REQUIRED`. By default, if no transaction has been set where the method was called, a new transaction is started (a new connection is created and executed). If a transaction has already been set at the calling site, the logic runs within the existing transaction (executed within the same connection). If an exception occurs, it rolls back and the rollback propagates to the caller. **Propagation.REQUIRED** is the default, so it can be omitted. However, if the method runs on a separate thread from the caller, a separate transaction is created and executed regardless of the propagation level. Since Spring internally stores transaction information in a ThreadLocal variable, transactions do not propagate to other threads.
* **Propagation.REQUIRES\_NEW**\
  Starts a new transaction every time (a new connection is created and executed). If a transaction is already set at the calling site, the existing transaction is suspended until the method finishes, and the new transaction runs independently. Even if an exception occurs in the new transaction, the rollback does not propagate to the caller.
* **Propagation.SUPPORTS**\
  Joins an already-started transaction if one exists; otherwise proceeds without a transaction.
* **Propagation.NESTED**\
  By default behaves the same as REQUIRED, but allows partial rollback up to a specified SAVEPOINT. Can only be used if the database supports the SAVEPOINT feature (e.g., Oracle).
* **Propagation.MANDATORY**\
  Joins an already-started transaction if one exists; otherwise throws an exception. Used when a transaction must not proceed independently.
* **Propagation.NOT\_SUPPORTED**\
  Does not use a transaction. Suspends any transaction that is already in progress.
* **Propagation.NEVER**\
  Forces no transaction to be used. Throws an exception if a transaction is already in progress.

Frequently used propagation settings: REQUIRED, REQUIRES\_NEW, NESTED, SUPPORTS. In general, use REQUIRED by default, and REQUIRES\_NEW to separate transactions.

## Setting Transaction Boundaries

ReadWrite transactions are determined by the AOP code below. Functions annotated with `@Transactional` operate as ReadWrite.

```java
/** AOP configuration for ReadWrite Transaction */
@Aspect
@Component
public class ReadWriteDatabaseTransactionAspect {
    @Around("@annotation(org.springframework.transaction.annotation.Transactional)")
    public Object logging(ProceedingJoinPoint pjp) throws Throwable {
        RoutingDatabaseContextHolder.set(RoutingDatabase.READWRITE);
        Object result;
        try {
            result = pjp.proceed();
        } finally {
            RoutingDatabaseContextHolder.clear();
        }
        return result;
    }
}
```

ReadOnly transactions are determined by the AOP code below. In the code below, the qualifier `within(@org.springframework.stereotype.Service *)` is declared so that only service classes without the `@Transactional` annotation participate in ReadOnly transactions.

By default, functions without the `@Transactional` annotation operate as ReadOnly, but if the starting function begins with a function annotated `@Transactional`, then even a query service without the `@Transactional` annotation will operate as ReadWrite.

```java
/** AOP configuration for READONLY Transaction */
@Aspect
@Component
public class ReadOnlyDatabaseTransactionAspect {
    @Around("within(@org.springframework.stereotype.Service *) && !@annotation(org.springframework.transaction.annotation.Transactional) && !@within(org.springframework.transaction.annotation.Transactional)")
    public Object logging(ProceedingJoinPoint pjp) throws Throwable {
        RoutingDatabase context = RoutingDatabaseContextHolder.getClientDatabase();
        if (context == RoutingDatabase.READWRITE) {
            RoutingDatabaseContextHolder.set(RoutingDatabase.READWRITE);
        } else {
            RoutingDatabaseContextHolder.set(RoutingDatabase.READONLY);
        }
        Object result;
        try {
            result = pjp.proceed();
        } finally {
            RoutingDatabaseContextHolder.clear();
        }
        return result;
    }
}
```

## Exception Rollback Handling

To roll back on all exceptions, the basic annotation structure is as follows.

```java
@Transactional(rollbackFor = {Exception.class})
```

In the declaration above, propagation = Propagation.REQUIRES(=REQUIRED) is the default.

For a transaction rollback to be triggered, the exception must basically be a RuntimeException or one of its subclasses. To catch a checked exception (e.g., Exception) and trigger a rollback, you must explicitly configure it, as in `rollbackFor = {Exception.class}`.

### Example 1

```java
public class SampleServiceImpl implements SampleService {
    @Override
    @Transactional(rollbackFor = {Exception.class})
    public List<TestLog> txTest4() throws Exception {
        // Save data READWRITE
        sampleService3.test2();
        // Update data READWRITE
        sampleService3.testupdate1();
        throw new Exception("Forced error occurred");
    }
}
...
public class SampleServiceImpl3 implements SampleService3 {
    private final SampleTrxMapper sampleTrxMapper;

    @Override
    @Transactional(rollbackFor = {Exception.class})
    public void test2() {
        TestLog test = new TestLog();
        test.setSeq(1);
        test.setLog(getValue());
        test.setTestValue(getValue());
        sampleTrxMapper.insertTestLog(test);
    }

    @Override
    @Transactional(rollbackFor = {Exception.class})
    public void testupdate1() {
        TestLog test = new TestLog();
        test.setSeq(1);
        test.setLog(getValue());
        test.setTestValue(getValue());
        sampleTrxMapper.updateTestLog(test);
    }
}
```

In the example above, Propagation.REQUIRED applies, so everything runs on a single transaction (connection); if an exception occurs, the entire transaction is rolled back.

### Example 2

```java
public class SampleServiceImpl implements SampleService {
    @Override
    @Transactional(rollbackFor = {Exception.class})
    public List<TestLog> txTest10() {
        // Save data READWRITE
        sampleService3.test2();
        try {
            // testinsert1 forces an Exception.
            // Because it's the same transaction, wrapping in try-catch still rolls back test2().
            sampleService3.testinsert1();
        } catch (Exception ex) {
        }
        // READWRITE query
        List<TestLog> list = sampleService2.findTestLog();
        log.debug("txTest10 size : {}", list.size());
        return list;
    }

    @Override
    @Transactional(rollbackFor = {Exception.class})
    public List<TestLog> txTest11() {
        // Save data READWRITE
        sampleService3.test2();
        try {
            // testinsert2 forces an Exception.
            // Because it's REQUIRES_NEW (a new transaction), test2() is not rolled back.
            sampleService3.testinsert2();
        } catch (Exception ex) {
        }
        // READWRITE query
        List<TestLog> list = sampleService2.findTestLog();
        log.debug("txTest11 size : {}", list.size());
        return list;
    }
}
```

* txTest10: The Exception raised in `sampleService3.testinsert1()` is in the same transaction, so everything is rolled back.
* txTest11: `sampleService3.testinsert2()` runs in a new transaction via `REQUIRES_NEW`, so only that part is rolled back, and `test2()`'s data is preserved.

### Example 3

```java
public class SampleServiceImpl implements SampleService {
    @Override
    @Transactional(rollbackFor = {Exception.class})
    public List<TestLog> txTest12() {
        for (int i = 0; i < 10; i++) {
            try {
                // Save data
                sampleService3.testinsertData(i);
                // Save success log (REQUIRES_NEW)
                sampleService3.testinsertSuccess();
            } catch (Exception ex) {
                // Save failure log (REQUIRES_NEW)
                sampleService3.testinsertFailure();
            }
        }
        // READWRITE query
        List<TestLog> list = sampleService2.findTestLog();
        log.debug("txTest12 size : {}", list.size());
        return list;
    }
}
```

In the example above, if an Exception occurs at the end of the loop in txTest12, most of the data saved during the loop is rolled back, but the success and failure logs are committed separately and saved, because they run in `REQUIRES_NEW` transactions.

<figure><img src="https://200425-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVEZx3rsZsIv89GPS3d2J%2Fuploads%2Fw0Cz5Rkof34TXRWL6Orf%2Fimage2023-5-9_5-10-20.png?alt=media&#x26;token=430a04db-9cd3-44de-9c87-b6f6858cc035" alt=""><figcaption></figcaption></figure>

## Separating Transactions

The annotation structure for separating a transaction at the DAO (Repository) call level within a Service function is as follows.

```java
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = {Exception.class})
```

Example:

```java
public class SampleServiceImpl implements SampleService {
    @Override
    @Transactional(rollbackFor = {Exception.class})
    public List<TestLog> txTest5() throws Exception {
        // Save data READWRITE
        sampleService3.test2();
        // Save data READWRITE (REQUIRES_NEW, new transaction)
        // Since test3 runs in a new transaction, its data is not rolled back.
        sampleService3.test3();
        throw new RuntimeException("Forced error occurred");
    }
}
...
public class SampleServiceImpl3 implements SampleService3 {
    private final SampleTrxMapper sampleTrxMapper;

    @Override
    @Transactional(rollbackFor = {Exception.class})
    public void test2() {
        TestLog test = new TestLog();
        test.setSeq(1);
        test.setLog(getValue());
        test.setTestValue(getValue());
        sampleTrxMapper.insertTestLog(test);
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = {Exception.class})
    public void test3() {
        TestLog test = new TestLog();
        test.setSeq(1);
        test.setLog(getValue());
        test.setTestValue(getValue());
        sampleTrxMapper.insertTestLog(test);
    }
}
```

In the example above, `test3()` runs in a separate transaction via `REQUIRES_NEW`, so even if an exception occurs outside it, the data saved by `test3()` is not rolled back. Conversely, `test2()`'s data can be affected by the outer transaction and rolled back.

{% hint style="info" %}

### Notes on Using @Transactional

1. **@Transactional(readOnly=true) Behavior**

When `@Transactional(readOnly=true)` is set, the connection is made to the ReadWrite database but operates as ReadOnly.

* Internally, `Connection.setReadOnly(true)` is called.

2. **Default Value of the readOnly Attribute and Its Relationship with JPA**
   * The default value of the readOnly attribute is false.
   * Within JPA's persistence context, when readOnly=true, dirty checking on entities is skipped at commit time, providing a performance benefit.
   * Therefore, use readOnly=true when querying JPA entities against a ReadWrite DB.
   * For code readability, it is recommended to always explicitly specify the readOnly=false/true attribute.
3. **Scope and Limitations of @Transactional**

* When `@Transactional` is declared at the class or method level, the specified transaction is applied when that method is called.
* However, there is a condition: `@Transactional` is recognized and applied only when the bean of that class is called from a bean of another class.
* Calling another `@Transactional`-annotated method within the same bean does not trigger it.
* Reason: Spring Framework internally recognizes the annotation via AOP and creates a proxy to automatically manage the transaction.

There are two ways to resolve this:

1. Structural change: split the ReadWrite and ReadOnly functions into two separate services, so that AOP is triggered each time each service is called, applying the appropriate ReadWrite / ReadOnly connection pool to each service.
2. Deferred lookup: use something like ObjectProvider to defer the lookup of the instance until the actual code runs, so that AOP is triggered.

Below is example code using deferred lookup (ObjectProvider).

```
@Service
@Slf4j
@RequiredArgsConstructor
public class SampleServiceImpl implements SampleService {

  private final ObjectProvider<SampleServiceImpl> sampleServiceObjectProvider;      

  @Override
  @Transactional(rollbackFor = {Exception.class})
  public List<TestLog> txTest13() throws Exception {  

       // Save data READWRITE  
       sampleService3.test2();

       // Calling a function within the same service does not apply transaction boundary settings.  
       testInsert();  

       throw new RuntimeException("Forced error occurred");  
  }

  @Override
  @Transactional(rollbackFor = {Exception.class})
  public List<TestLog> txTest14() throws Exception {

       // Save data READWRITE  
       sampleService3.test2();

       // Using ObjectProvider for deferred lookup does apply transaction boundary settings,  
       // but Spring recommends the structural-change approach of splitting services instead.  
       SampleService sempleService = sampleServiceObjectProvider.getObject();
       sempleService.testInsert();

       throw new RuntimeException("Forced error occurred");  
  }

  @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = {Exception.class})
  public void testInsert() {

       TestLog test = new TestLog();
       test.setSeq(1);
       test.setLog(getValue());
       test.setTestValue(getValue());
       sampleTrxMapper.insertTestLog(test);
  }

}
```

In the example above, when the `txTest13()` function is called, a function within the same service is called, so the transaction boundary setting does not take effect — the `REQUIRES_NEW` option on `testInsert()` is not applied, and all data is rolled back.

However, for the `txTest14()` function, because the deferred-lookup method ObjectProvider is used, the `REQUIRES_NEW` transaction boundary is applied correctly even when calling a function within the same service — so `test2()`'s data is rolled back, but `testInsert()`'s data is saved normally.

**Within Spring, the structural-change approach (a) is preferred over the deferred-lookup approach (b).**<br>
{% endhint %}

## Connecting Multiple Data Sources

A method for connecting multiple data sources in a single transaction using different DB schemas and TransactionManagers (e.g., how and why `@Transactional rollbackFor` is used between orderrwdb and drmcrwdb in api-member)

Service example:

```java
@Service
@Slf4j
@RequiredArgsConstructor
public class SampleServiceImpl implements SampleService {
    @Transactional(value = "chainedTransactionManager", rollbackFor = {Exception.class})
    public void rollbackFor4(Test test) throws Exception {
        try {
            insertSample1(test); // orderrwdb insert
            insertSample2(test); // drmcrwdb insert
            throw new Exception("Forced error occurred");
        } catch (Exception e) {
            throw AppException.exception(ApiError.FAIL_ERROR_INTEGRATE_WITHDRAWAL);
        }
    }
}
```

When using `@Transactional`, specify `value = "chainedTransactionManager"`.

Example ChainedTransactionManager configuration:

```java
import org.springframework.data.transaction.ChainedTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

public class ChainedTransactionConfig {
    @Bean
    @Primary
    public PlatformTransactionManager chainedTransactionManager(
        @Qualifier("drmcRwdbTxManager") PlatformTransactionManager firstTxManager,
        @Qualifier("orderRwdbTxManager") PlatformTransactionManager secondTxManager) {
        return new ChainedTransactionManager(firstTxManager, secondTxManager);
    }
}
```

* `ChainedTransactionManager` is provided by org.springframework.data (Spring Data Commons); it chains multiple transaction managers together (performing Start/Commit sequentially) so they behave as a single transaction.
* However, `ChainedTransactionManager` does not provide a "perfect" transaction, so care is needed — for example, place transactions with a larger error impact later in the chain.

***

##
