1. Overview
Java Transaction API, more commonly known as JTA, is an API for managing transactions in Java. It allows us to start, commit and rollback transactions in a resource-agnostic way.
The true power of JTA lies in its ability to manage multiple resources (i.e. databases, messaging services) in a single transaction.
In this tutorial, we’ll get to know JTA at the conceptual level and see how business code commonly interacts with JTA.
2. Universal API and Distributed Transaction
JTA provides an abstraction over transaction control (begin, commit and rollback) to business code.
In the absence of this abstraction, we’d have to deal with the individual APIs of each resource type.
For example, we need to deal with JDBC resource like this. Likewise, a JMS resource may have a similar but incompatible model.
With JTA, we can manage multiple resources of different types in a consistent and coordinated manner.
As an API, JTA defines interfaces and semantics to be implemented by transaction managers. Implementations are provided by libraries such as Narayana and Atomikos.
3. Sample Project Setup
The sample application is a very simple back-end service of a banking application. We have two services, the BankAccountService and AuditService using two different databases**.** These independent databases need to be coordinated upon transaction begin, commit or rollback.
To begin with, our sample project uses Spring Boot to simplify configuration:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.6</version>
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
Finally, before each test method we initialize AUDIT_LOG with empty data and database ACCOUNT with 2 rows:
+-----------+----------------+
| ID | BALANCE |
+-----------+----------------+
| a0000001 | 1000 |
| a0000002 | 2000 |
+-----------+----------------+
4. Declarative Transaction Demarcation
The first way of working with transactions in JTA is with the use of the @Transactional annotation. For a more elaborate explanation and configuration see this article.
Let’s annotate the facade service method executeTranser() with @Transactional. This instructs the transaction manager to begin a transaction*:*
@Transactional
public void executeTransfer(String fromAccontId, String toAccountId, BigDecimal amount) {
bankAccountService.transfer(fromAccontId, toAccountId, amount);
auditService.log(fromAccontId, toAccountId, amount);
...
}
Here the method executeTranser() calls 2 different services, AccountService and AuditService. These services use 2 different databases.
When executeTransfer() returns, the transaction manager recognizes that it is the end of the transaction and will commit to both databases:
tellerService.executeTransfer("a0000001", "a0000002", BigDecimal.valueOf(500));
assertThat(accountService.balanceOf("a0000001"))
.isEqualByComparingTo(BigDecimal.valueOf(500));
assertThat(accountService.balanceOf("a0000002"))
.isEqualByComparingTo(BigDecimal.valueOf(2500));
TransferLog lastTransferLog = auditService
.lastTransferLog();
assertThat(lastTransferLog)
.isNotNull();
assertThat(lastTransferLog.getFromAccountId())
.isEqualTo("a0000001");
assertThat(lastTransferLog.getToAccountId())
.isEqualTo("a0000002");
assertThat(lastTransferLog.getAmount())
.isEqualByComparingTo(BigDecimal.valueOf(500));
4.1. Rolling Back in Declarative Demarcation
At the end of the method, executeTransfer() checks the account balance and throws RuntimeException if the source fund is insufficient:
@Transactional
public void executeTransfer(String fromAccontId, String toAccountId, BigDecimal amount) {
bankAccountService.transfer(fromAccontId, toAccountId, amount);
auditService.log(fromAccontId, toAccountId, amount);
BigDecimal balance = bankAccountService.balanceOf(fromAccontId);
if(balance.compareTo(BigDecimal.ZERO) < 0) {
throw new RuntimeException("Insufficient fund.");
}
}
An unhandled RuntimeException past the first @Transactional will rollback the transaction to both databases. In effect, executing a transfer with an amount bigger than the balance will cause a rollback**:**
assertThatThrownBy(() -> {
tellerService.executeTransfer("a0000002", "a0000001", BigDecimal.valueOf(10000));
}).hasMessage("Insufficient fund.");
assertThat(accountService.balanceOf("a0000001")).isEqualByComparingTo(BigDecimal.valueOf(1000));
assertThat(accountService.balanceOf("a0000002")).isEqualByComparingTo(BigDecimal.valueOf(2000));
assertThat(auditServie.lastTransferLog()).isNull();
5. Programmatic Transaction Demarcation
Another way to control JTA transaction is programmatically via UserTransaction.
Now let’s modify executeTransfer() to handle transaction manually:
userTransaction.begin();
bankAccountService.transfer(fromAccontId, toAccountId, amount);
auditService.log(fromAccontId, toAccountId, amount);
BigDecimal balance = bankAccountService.balanceOf(fromAccontId);
if(balance.compareTo(BigDecimal.ZERO) < 0) {
userTransaction.rollback();
throw new RuntimeException("Insufficient fund.");
} else {
userTransaction.commit();
}
In our example, the begin() method starts a new transaction. If the balance validation fails, we call rollback() which will rollback over both databases. Otherwise, the call to commit() commits the changes to both databases.
It’s important to note that both commit() and rollback() end the current transaction.
Ultimately, using programmatic demarcation gives us the flexibility of fine-grained transaction control.
6. Conclusion
In this article, we discussed the problem JTA tries to resolve. The code examples illustrate controlling transaction with annotations and programmatically, involving 2 transactional resources that need to be coordinated in a single transaction.
As usual, the code example can be found over on GitHub.