This topic simulates a fund transfer scenario and implements the transfer feature using the TCC, SAGA, and FMT patterns for distributed transactions. It also provides a general overview of the performance of these patterns based on stress testing results.
Stress testing model
As shown in the following figure, Application A exposes a transfer service. The service transfers funds from Account A to Account B.

The implementation flow is as follows:
The transfer service starts a distributed transaction.
Participant A, which is provided by Application A (the initiator), debits funds from Account A.
Participant B, which is provided by Application B, credits funds to Account B.
The account data for Account A is stored in Database A and is accessed only by Application A. The account data for Account B is stored in Database B and is accessed by Application B.
Implement the transfer
Table schema design
The schema of table A in the database is as follows:
## Account balance table, which stores account balance information
create table account(
account_no varchar(256)notnull comment 'Account',
amount DOUBLE comment 'Account balance',
freezed_amount DOUBLE comment 'Frozen amount. This field is used only in the TCC pattern.',
primary key (account_no)
);
## Account transaction log table, which records the account, amount, and operation type (debit/credit) for each distributed transaction operation. This table is used only in the TCC pattern.
create table account_transaction(
tx_id varchar(256)notnull,
account_no varchar(256)notnull,
amount DOUBLE,
type varchar(256)notnull,
primary key (tx_id)
);The schema of table B is the same as that of table A.
Implementing the TCC pattern
Initiator implementation:
@DtxTransaction(bizType = "single-transfer-by-tcc")
public boolean transferByTcc(String from, String to, double amount) {
try {
// First participant
boolean ret = firstTccActionRef.prepare_minus(null, from, amount);
if (!ret) {
// Transaction rollback
RuntimeContext.setRollBack();
return false;
}
// Second participant
ret = secondTccActionRef.prepare_add(null, to, amount);
if (!ret) {
// Transaction rollback
RuntimeContext.setRollBack();
return false;
}
return ret;
} catch (Throwable t) {
throw new RuntimeException(t);
}
}Participant A (debit) implementation:
public interface FirstTccAction {
@TwoPhaseBusinessAction(name = "firstTccAction", commitMethod = "commit", rollbackMethod = "rollback")
public boolean prepare_minus(BusinessActionContext businessActionContext, String accountNo,
double amount);
public boolean commit(BusinessActionContext businessActionContext);
public boolean rollback(BusinessActionContext businessActionContext);
}Implementation for Participant B (Payer):
public interface SecondTccAction {
@TwoPhaseBusinessAction(name = "secondTccAction", commitMethod = "commit", rollbackMethod = "rollback")
public boolean prepare_add(BusinessActionContext businessActionContext, String accountNo, double amount);
public boolean commit(BusinessActionContext businessActionContext);
public boolean rollback(BusinessActionContext businessActionContext);
}Implementing the FMT pattern
Initiator implementation:
@DtxTransaction(bizType = "transfer-by-auto")
public boolean transferByAuto(String from, String to, double amount) {
boolean ret = false;
try {
// First participant
ret = firstAutoAction.amount_minus(from, amount);
if (!ret) {
// Transaction rollback
RuntimeContext.setRollBack();
return false;
}
// Second participant
ret = secondAutoAction.amount_add(to, amount);
if (!ret) {
// Transaction rollback
RuntimeContext.setRollBack();
return false;
}
return ret;
} catch (Throwable t) {
throw new RuntimeException(t);
}
}Implementation of Participant A (debit):
public class FirstAutoActionImpl implements FirstAutoAction {
......
@Override
public boolean amount_minus(final String accountNo, final double amount) {
try {
return tccFirstActionTransactionTemplateAuto.execute(new TransactionCallback<Boolean>() {
@Override
public Boolean doInTransaction(TransactionStatus status) {
try {
Account account = firstAccountDAOAuto.getAccountForUpdate(accountNo);
if (account == null) {
throw new RuntimeException("Account does not exist");
}
// Debit
double newAmount = account.getAmount() - amount;
if (amount < 0) {
throw new RuntimeException("Insufficient balance");
}
account.setAmount(newAmount);
int n = firstAccountDAOAuto.updateAmount(account);
if (n == 1) {
return true;
} else {
status.setRollbackOnly();
return false;
}
} catch (Exception e) {
logger.error("amount_minus error", e);
status.setRollbackOnly();
return false;
}
}
});
} catch (Exception e) {
logger.error("amount_minus failed", e);
return false;
}
}
}Implementing Participant B (Payer):
public class SecodeAutoActionImpl implements SecondAutoAction {
......
@Override
public boolean amount_add(final String accountNo, final double amount) {
try {
return tccSecondActionTransactionTemplateAuto.execute(new TransactionCallback < Boolean > () {
@Override
public Boolean doInTransaction (TransactionStatus status){
try {
Account account = secondAccountDAOAuto.getAccountForUpdate(accountNo);
if (account == null) {
throw new RuntimeException("Account does not exist");
}
// Credit
double newAmount = account.getAmount() + amount;
account.setAmount(newAmount);
secondAccountDAOAuto.updateAmount(account);
} catch (Exception e) {
logger.error("amount_add error", e);
status.setRollbackOnly();
return false;
}
return true;
}
});
} catch (Exception e) {
logger.error("amount_add failed", e);
return false;
}
}
}Implement the SAGA pattern
You can use a SAGA state machine to design the business flow, as shown in the following figure:
amountMinusis the debit service.amountAddis the service used to add money.compensateMinusis the compensation (reversal) service that cancels a debit transaction.compensateAddis the compensation service that cancels a credit transaction.

Initiator implementation:
public class TransferServiceImpl implements TransferService {
protected final static Logger logger = LoggerFactory.getLogger(TransferServiceImpl.class);
@Autowired
private StateMachineEngine stateMachineEngine;
@Override
public boolean transferBySaga(String from, String to, BigDecimal amount) {
try {
// Generate a business ID
String businessKey = UUID.randomUUID().toString().replaceAll("-", "");
Map<String, Object> params = new HashMap<>(4);
params.put("from", from); // Transfer-out account
params.put("to", to); // Transfer-in account
params.put("amount", amount); // Transfer amount
StateMachineInstance inst = stateMachineEngine.startWithBusinessKey("transferBySaga", null, businessKey, params);
if (ExecutionStatus.SU.equals(inst.getStatus())
&& inst.getCompensationStatus() == null) {
// If the forward status is successful and the compensation status is null (rollback is not triggered), the transaction is successful.
return true;
} else {
// If the flow reaches the Fail node, an errorCode and an errorMessage are generated.
String errorCode = (String) inst.getContext().get(DomainConstants.VAR_NAME_STATEMACHINE_ERROR_CODE);
String errorMessage = (String) inst.getContext().get(DomainConstants.VAR_NAME_STATEMACHINE_ERROR_MSG);
System.out.println("ErrorCode:" + errorCode + ", ErrorMsg:" + errorMessage + ", exception: " + inst.getException());
return false;
}
} catch (Throwable t) {
logger.error("Transfer transaction failed", t);
throw new RuntimeException(t);
}
}
}Participant A (debit) implementation:
public class FirstSagaActionImpl implements FirstSagaAction {
protected final staticLogger logger = LoggerFactory.getLogger(FirstSagaActionImpl.class);
// Account DAO
private AccountDAO firstAccountDAO;
// Business transaction log DAO
private AccountTransactionDAO firstAccountTransactionDAO;
// Transaction template
private TransactionTemplate firstActionTransactionTemplate;
/**
* Debit operation
**/
@Override
public boolean amountMinus(final String businessKey, final String accountNo, final BigDecimal amount, final Map<String, Object> extParams) {
try {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new RuntimeException("Amount must be greater than 0");
}
AccountTransaction accountTransaction = firstAccountTransactionDAO.findTransaction(businessKey);
if (accountTransaction != null && Status.SUCCESS.name().equals(accountTransaction.getStatus())) {
// Idempotence control: If the transaction is successful, return true.
return true;
}
return firstActionTransactionTemplate.execute(new TransactionCallback<Boolean>() {
@Override
public Boolean doInTransaction(TransactionStatus status) {
try {
// Check the account balance
Account account = firstAccountDAO.getAccountForUpdate(accountNo);
if (account.getAmount().compareTo(amount) < 0) {
throw new RuntimeException("Insufficient balance");
}
// Record the account transaction log
AccountTransaction accountTransaction = new AccountTransaction();
accountTransaction.setBusinessKey(businessKey);
accountTransaction.setAccountNo(accountNo);
accountTransaction.setAmount(amount);
accountTransaction.setType(Type.MINUS.name());
accountTransaction.setStatus(Status.SUCCESS.name());
firstAccountTransactionDAO.addTransaction(accountTransaction);
// Debit
BigDecimal amount = account.getAmount().subtract(accountTransaction.getAmount());
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new RuntimeException("Insufficient balance");
}
account.setAmount(amount);
firstAccountDAO.updateAmount(account);
} catch (Exception e) {
logger.error("Debit operation failed", e);
throw new RuntimeException("Debit operation failed", e);
}
return true;
}
});
} catch (Exception e) {
logger.error("Debit operation failed", e);
return false;
}
}
/**
* Compensate (reverse) the debit operation
**/
@Override
public boolean compensateAmountMinus(final String businessKey, final String accountNo) {
AccountTransaction accountTransaction;
try {
accountTransaction = firstAccountTransactionDAO.findTransaction(businessKey);
if (accountTransaction == null) {
// The original transaction log does not exist. Record a log to prevent dangling transactions.
accountTransaction = new AccountTransaction();
accountTransaction.setBusinessKey(businessKey);
accountTransaction.setAccountNo(accountNo);
accountTransaction.setType(Type.MINUS.name());
accountTransaction.setStatus(Status.COMPENSATED.name());
firstAccountTransactionDAO.addTransaction(accountTransaction);
return true;
}
if (Status.COMPENSATED.name().equals(accountTransaction.getStatus())) {
// Idempotence control: If the compensation is successful, return true.
return true;
}
final AccountTransaction accountTransactionFinal = accountTransaction;
return firstActionTransactionTemplate.execute(new TransactionCallback<Boolean>() {
@Override
public Boolean doInTransaction(TransactionStatus status) {
try {
// Compensate the debited amount
Account account = firstAccountDAO.getAccountForUpdate(accountTransactionFinal.getAccountNo());
BigDecimal amount = account.getAmount().add(accountTransactionFinal.getAmount());
account.setAmount(amount);
firstAccountDAO.updateAmount(account);
// Update the log status
accountTransactionFinal.setStatus(Status.COMPENSATED.name());
firstAccountTransactionDAO.updateTransaction(accountTransactionFinal);
return true;
} catch (Exception e) {
logger.error("Debit operation compensation failed", e);
status.setRollbackOnly();
return false;
}
}
});
} catch (SQLException e) {
logger.error("Debit operation compensation failed", e);
return false;
}
}
}Implementation for Participant B (Payer):
public class SecondSagaActionImpl implements SecondSagaAction {
protected final static Logger logger = LoggerFactory.getLogger(SecondSagaActionImpl.class);
private AccountDAO secondAccountDAO;
private AccountTransactionDAO secondAccountTransactionDAO;
private TransactionTemplate secondActionTransactionTemplate;
/**
* Credit operation
**/
@Override
public boolean amountAdd(finalString businessKey, final String accountNo, final BigDecimal amount, final Map<String, Object> extParams) {
try {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new RuntimeException("Amount must be greater than 0");
}
AccountTransaction accountTransaction = secondAccountTransactionDAO.findTransaction(businessKey);
if (accountTransaction != null && Status.SUCCESS.name().equals(accountTransaction.getStatus())) {
// Idempotence control: If the transaction is successful, return true.
return true;
}
return secondActionTransactionTemplate.execute(new TransactionCallback<Boolean>() {
@Override
public Boolean doInTransaction(TransactionStatus status) {
try {
// Record the account transaction log
AccountTransaction accountTransaction = new AccountTransaction();
accountTransaction.setBusinessKey(businessKey);
accountTransaction.setAccountNo(accountNo);
accountTransaction.setAmount(amount);
accountTransaction.setType(Type.ADD.name());
accountTransaction.setStatus(Status.SUCCESS.name());
secondAccountTransactionDAO.addTransaction(accountTransaction);
Account account = secondAccountDAO.getAccountForUpdate(accountNo);
// Credit
BigDecimal amount = account.getAmount().add(accountTransaction.getAmount());
account.setAmount(amount);
secondAccountDAO.updateAmount(account);
} catch (Exception e) {
logger.error("Credit operation failed", e);
throw new RuntimeException("Credit operation failed", e);
}
return true;
}
});
} catch (Exception e) {
logger.error("Credit operation failed", e);
return false;
}
}
/**
* Compensate (reverse) the credit operation
**/
@Override
public boolean compensateAmountAdd(final String businessKey, final String accountNo) {
AccountTransaction accountTransaction;
try {
accountTransaction = secondAccountTransactionDAO.findTransaction(businessKey);
if (accountTransaction == null) {
// Dangling prevention: The original transaction log does not exist. Record a log to prevent dangling transactions.
accountTransaction = new AccountTransaction();
accountTransaction.setBusinessKey(businessKey);
accountTransaction.setAccountNo(accountNo);
accountTransaction.setType(Type.ADD.name());
accountTransaction.setStatus(Status.COMPENSATED.name());
secondAccountTransactionDAO.addTransaction(accountTransaction);
// Allow null compensation: Return true for successful compensation.
return true;
}
if (Status.COMPENSATED.name().equals(accountTransaction.getStatus())) {
// Idempotence control: If the compensation is successful, return true.
return true;
}
final AccountTransaction accountTransactionFinal = accountTransaction;
return secondActionTransactionTemplate.execute(new TransactionCallback < Boolean > () {
@Override
public Boolean doInTransaction (TransactionStatus status){
try {
// Debit the credited amount
Account account = secondAccountDAO.getAccountForUpdate(accountTransactionFinal.getAccountNo());
BigDecimal amount = account.getAmount().subtract(accountTransactionFinal.getAmount());
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new RuntimeException("Insufficient balance. Please check manually.");
}
account.setAmount(amount);
secondAccountDAO.updateAmount(account);
// Update the log status
accountTransactionFinal.setStatus(Status.COMPENSATED.name());
secondAccountTransactionDAO.updateTransaction(accountTransactionFinal);
return true;
} catch (Exception e) {
logger.error("Credit operation compensation failed", e);
status.setRollbackOnly();
return false;
}
}
});
} catch (SQLException e) {
logger.error("Credit operation compensation failed", e);
return false;
}
}
}Hardware environment
The hardware configuration for this scenario is as follows:
RDS uses a MySQL 5.6 database.

Stress testing data policy
Data scale (number of accounts): This test transfers funds from Group A to Group B to evaluate the performance of the TCC, SAGA, and FMT distributed transaction patterns across different data scales.
Stress testing pattern | Number of accounts in Group A | Number of accounts in Group B |
TCC, FMT, SAGA | 5,000 | 5,000 |
Stress testing results
The following tables show the TPS and response times for the TCC, SAGA, and FMT patterns for a fund transfer from Group A to Group B.
TCC (Total) and TCC (Success) are the total and successful TPS for the TCC pattern, respectively. The same applies to the FMT and SAGA patterns.
TPS
User volume (Group A to Group B)
TPS
Group A: 5,000 Group B: 5,000
Concurrency
TCC (Total)
TCC (Success)
FMT (Total)
FMT (Success)
SAGA (Total)
SAGA (Success)
1
50
50
40
40
48
48
5
250
250
200
200
240
240
10
460
460
380
380
450
450
20
800
800
695
690
780
780
50
1450
1450
1220
1200
1300
1300
100
1500
1500
1550
1500
1480
1480
200
1500
1500
1600
1450
1550
1550
500
1600
1600
1500
1400
1580
1580
700
1500
1500
1500
1300
1550
1550

Response time
Response Time (Success)
Concurrency
TCC
FMT
SAGA
1
19
26
21
5
21
25
21
10
23
27
23
20
27
30
26
50
35
45
38
100
70
75
68
200
135
160
129
500
225
380
316
700
490
520
451
