diff --git a/jta/pom.xml b/jta/pom.xml
new file mode 100644
index 0000000000..89bdccf25e
--- /dev/null
+++ b/jta/pom.xml
@@ -0,0 +1,88 @@
+
+
+ 4.0.0
+ com.baeldung
+ jta-demo
+ 1.0-SNAPSHOT
+ jar
+
+ JEE JTA demo
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.0.4.RELEASE
+
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-jta-bitronix
+
+
+ org.springframework.boot
+ spring-boot-starter-jdbc
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.hsqldb
+ hsqldb
+ 2.4.1
+
+
+
+
+
+ autoconfiguration
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ integration-test
+
+ test
+
+
+
+ **/*LiveTest.java
+ **/*IntegrationTest.java
+ **/*IntTest.java
+
+
+ **/AutoconfigurationTest.java
+
+
+
+
+
+
+ json
+
+
+
+
+
+
+
+
diff --git a/jta/src/main/java/com/baeldung/jtademo/JtaDemoApplication.java b/jta/src/main/java/com/baeldung/jtademo/JtaDemoApplication.java
new file mode 100644
index 0000000000..4d8779efe5
--- /dev/null
+++ b/jta/src/main/java/com/baeldung/jtademo/JtaDemoApplication.java
@@ -0,0 +1,48 @@
+package com.baeldung.jtademo;
+
+import org.hsqldb.jdbc.pool.JDBCXADataSource;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.jta.bitronix.BitronixXADataSourceWrapper;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+import javax.sql.DataSource;
+
+@EnableAutoConfiguration
+@EnableTransactionManagement
+@Configuration
+@ComponentScan
+public class JtaDemoApplication {
+
+ @Bean("dataSourceAccount")
+ public DataSource dataSource() throws Exception {
+ return createHsqlXADatasource("jdbc:hsqldb:mem:accountDb");
+ }
+
+ @Bean("dataSourceAudit")
+ public DataSource dataSourceAudit() throws Exception {
+ return createHsqlXADatasource("jdbc:hsqldb:mem:auditDb");
+ }
+
+ private DataSource createHsqlXADatasource(String connectionUrl) throws Exception {
+ JDBCXADataSource dataSource = new JDBCXADataSource();
+ dataSource.setUrl(connectionUrl);
+ dataSource.setUser("sa");
+ BitronixXADataSourceWrapper wrapper = new BitronixXADataSourceWrapper();
+ return wrapper.wrapDataSource(dataSource);
+ }
+
+ @Bean("jdbcTemplateAccount")
+ public JdbcTemplate jdbcTemplate(@Qualifier("dataSourceAccount") DataSource dataSource) {
+ return new JdbcTemplate(dataSource);
+ }
+
+ @Bean("jdbcTemplateAudit")
+ public JdbcTemplate jdbcTemplateAudit(@Qualifier("dataSourceAudit") DataSource dataSource) {
+ return new JdbcTemplate(dataSource);
+ }
+}
diff --git a/jta/src/main/java/com/baeldung/jtademo/dto/TransferLog.java b/jta/src/main/java/com/baeldung/jtademo/dto/TransferLog.java
new file mode 100644
index 0000000000..cc1474ce81
--- /dev/null
+++ b/jta/src/main/java/com/baeldung/jtademo/dto/TransferLog.java
@@ -0,0 +1,27 @@
+package com.baeldung.jtademo.dto;
+
+import java.math.BigDecimal;
+
+public class TransferLog {
+ private String fromAccountId;
+ private String toAccountId;
+ private BigDecimal amount;
+
+ public TransferLog(String fromAccountId, String toAccountId, BigDecimal amount) {
+ this.fromAccountId = fromAccountId;
+ this.toAccountId = toAccountId;
+ this.amount = amount;
+ }
+
+ public String getFromAccountId() {
+ return fromAccountId;
+ }
+
+ public String getToAccountId() {
+ return toAccountId;
+ }
+
+ public BigDecimal getAmount() {
+ return amount;
+ }
+}
diff --git a/jta/src/main/java/com/baeldung/jtademo/services/AuditService.java b/jta/src/main/java/com/baeldung/jtademo/services/AuditService.java
new file mode 100644
index 0000000000..f6810e15c8
--- /dev/null
+++ b/jta/src/main/java/com/baeldung/jtademo/services/AuditService.java
@@ -0,0 +1,33 @@
+package com.baeldung.jtademo.services;
+
+import com.baeldung.jtademo.dto.TransferLog;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.ResultSetExtractor;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+
+@Service
+public class AuditService {
+
+ final JdbcTemplate jdbcTemplate;
+
+ @Autowired
+ public AuditService(@Qualifier("jdbcTemplateAudit") JdbcTemplate jdbcTemplate) {
+ this.jdbcTemplate = jdbcTemplate;
+ }
+
+ public void log(String fromAccount, String toAccount, BigDecimal amount) {
+ jdbcTemplate.update("insert into AUDIT_LOG(FROM_ACCOUNT, TO_ACCOUNT, AMOUNT) values ?,?,?", fromAccount, toAccount, amount);
+ }
+
+ public TransferLog lastTransferLog() {
+ return jdbcTemplate.query("select FROM_ACCOUNT,TO_ACCOUNT,AMOUNT from AUDIT_LOG order by ID desc", (ResultSetExtractor) (rs) -> {
+ if (!rs.next())
+ return null;
+ return new TransferLog(rs.getString(1), rs.getString(2), BigDecimal.valueOf(rs.getDouble(3)));
+ });
+ }
+}
diff --git a/jta/src/main/java/com/baeldung/jtademo/services/BankAccountService.java b/jta/src/main/java/com/baeldung/jtademo/services/BankAccountService.java
new file mode 100644
index 0000000000..0c881edbaa
--- /dev/null
+++ b/jta/src/main/java/com/baeldung/jtademo/services/BankAccountService.java
@@ -0,0 +1,32 @@
+package com.baeldung.jtademo.services;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.ResultSetExtractor;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+
+@Service
+public class BankAccountService {
+
+ final JdbcTemplate jdbcTemplate;
+
+ @Autowired
+ public BankAccountService(@Qualifier("jdbcTemplateAccount") JdbcTemplate jdbcTemplate) {
+ this.jdbcTemplate = jdbcTemplate;
+ }
+
+ public void transfer(String fromAccountId, String toAccountId, BigDecimal amount) {
+ jdbcTemplate.update("update ACCOUNT set BALANCE=BALANCE-? where ID=?", amount, fromAccountId);
+ jdbcTemplate.update("update ACCOUNT set BALANCE=BALANCE+? where ID=?", amount, toAccountId);
+ }
+
+ public BigDecimal balanceOf(String accountId) {
+ return jdbcTemplate.query("select BALANCE from ACCOUNT where ID=?", new Object[] { accountId }, (ResultSetExtractor) (rs) -> {
+ rs.next();
+ return new BigDecimal(rs.getDouble(1));
+ });
+ }
+}
diff --git a/jta/src/main/java/com/baeldung/jtademo/services/TellerService.java b/jta/src/main/java/com/baeldung/jtademo/services/TellerService.java
new file mode 100644
index 0000000000..d3bd80a2ee
--- /dev/null
+++ b/jta/src/main/java/com/baeldung/jtademo/services/TellerService.java
@@ -0,0 +1,45 @@
+package com.baeldung.jtademo.services;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.transaction.Transactional;
+import javax.transaction.UserTransaction;
+import java.math.BigDecimal;
+
+@Service
+public class TellerService {
+ private final BankAccountService bankAccountService;
+ private final AuditService auditService;
+ private final UserTransaction userTransaction;
+
+ @Autowired
+ public TellerService(BankAccountService bankAccountService, AuditService auditService, UserTransaction userTransaction) {
+ this.bankAccountService = bankAccountService;
+ this.auditService = auditService;
+ this.userTransaction = userTransaction;
+ }
+
+ @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.");
+ }
+ }
+
+ public void executeTransferProgrammaticTx(String fromAccontId, String toAccountId, BigDecimal amount) throws Exception {
+ 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();
+ }
+ }
+}
diff --git a/jta/src/main/java/com/baeldung/jtademo/services/TestHelper.java b/jta/src/main/java/com/baeldung/jtademo/services/TestHelper.java
new file mode 100644
index 0000000000..c1e8e355ec
--- /dev/null
+++ b/jta/src/main/java/com/baeldung/jtademo/services/TestHelper.java
@@ -0,0 +1,43 @@
+package com.baeldung.jtademo.services;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.core.io.DefaultResourceLoader;
+import org.springframework.core.io.Resource;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.datasource.init.ScriptUtils;
+import org.springframework.stereotype.Component;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.SQLException;
+
+@Component
+public class TestHelper {
+ final JdbcTemplate jdbcTemplateAccount;
+
+ final JdbcTemplate jdbcTemplateAudit;
+
+ @Autowired
+ public TestHelper(@Qualifier("jdbcTemplateAccount") JdbcTemplate jdbcTemplateAccount, @Qualifier("jdbcTemplateAudit") JdbcTemplate jdbcTemplateAudit) {
+ this.jdbcTemplateAccount = jdbcTemplateAccount;
+ this.jdbcTemplateAudit = jdbcTemplateAudit;
+ }
+
+ public void runAccountDbInit() throws SQLException {
+ runScript("account.sql", jdbcTemplateAccount.getDataSource());
+ }
+
+ public void runAuditDbInit() throws SQLException {
+ runScript("audit.sql", jdbcTemplateAudit.getDataSource());
+ }
+
+ private void runScript(String scriptName, DataSource dataSouorce) throws SQLException {
+ DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
+ Resource script = resourceLoader.getResource(scriptName);
+ try (Connection con = dataSouorce.getConnection()) {
+ ScriptUtils.executeSqlScript(con, script);
+ }
+ }
+
+}
diff --git a/jta/src/main/resources/account.sql b/jta/src/main/resources/account.sql
new file mode 100644
index 0000000000..af14f89b01
--- /dev/null
+++ b/jta/src/main/resources/account.sql
@@ -0,0 +1,9 @@
+DROP SCHEMA PUBLIC CASCADE;
+
+create table ACCOUNT (
+ID char(8) PRIMARY KEY,
+BALANCE NUMERIC(28,10)
+);
+
+insert into ACCOUNT(ID, BALANCE) values ('a0000001', 1000);
+insert into ACCOUNT(ID, BALANCE) values ('a0000002', 2000);
\ No newline at end of file
diff --git a/jta/src/main/resources/application.properties b/jta/src/main/resources/application.properties
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/jta/src/main/resources/audit.sql b/jta/src/main/resources/audit.sql
new file mode 100644
index 0000000000..aa5845f402
--- /dev/null
+++ b/jta/src/main/resources/audit.sql
@@ -0,0 +1,8 @@
+DROP SCHEMA PUBLIC CASCADE;
+
+create table AUDIT_LOG (
+ID INTEGER IDENTITY PRIMARY KEY,
+FROM_ACCOUNT varchar(8),
+TO_ACCOUNT varchar(8),
+AMOUNT numeric(28,10)
+);
\ No newline at end of file
diff --git a/jta/src/test/java/com/baeldung/jtademo/JtaDemoUnitTest.java b/jta/src/test/java/com/baeldung/jtademo/JtaDemoUnitTest.java
new file mode 100644
index 0000000000..3f6004262b
--- /dev/null
+++ b/jta/src/test/java/com/baeldung/jtademo/JtaDemoUnitTest.java
@@ -0,0 +1,91 @@
+package com.baeldung.jtademo;
+
+import com.baeldung.jtademo.dto.TransferLog;
+import com.baeldung.jtademo.services.AuditService;
+import com.baeldung.jtademo.services.BankAccountService;
+import com.baeldung.jtademo.services.TellerService;
+import com.baeldung.jtademo.services.TestHelper;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.math.BigDecimal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(classes = JtaDemoApplication.class)
+public class JtaDemoUnitTest {
+ @Autowired
+ TestHelper testHelper;
+
+ @Autowired
+ TellerService tellerService;
+
+ @Autowired
+ BankAccountService accountService;
+
+ @Autowired
+ AuditService auditService;
+
+ @Before
+ public void beforeTest() throws Exception {
+ testHelper.runAuditDbInit();
+ testHelper.runAccountDbInit();
+ }
+
+ @Test
+ public void givenAnnotationTx_whenNoException_thenAllCommitted() throws Exception {
+ 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));
+ }
+
+ @Test
+ public void givenAnnotationTx_whenException_thenAllRolledBack() throws Exception {
+ assertThatThrownBy(() -> {
+ tellerService.executeTransfer("a0000002", "a0000001", BigDecimal.valueOf(100000));
+ }).hasMessage("Insufficient fund.");
+
+ assertThat(accountService.balanceOf("a0000001")).isEqualByComparingTo(BigDecimal.valueOf(1000));
+ assertThat(accountService.balanceOf("a0000002")).isEqualByComparingTo(BigDecimal.valueOf(2000));
+ assertThat(auditService.lastTransferLog()).isNull();
+ }
+
+ @Test
+ public void givenProgrammaticTx_whenCommit_thenAllCommitted() throws Exception {
+ tellerService.executeTransferProgrammaticTx("a0000001", "a0000002", BigDecimal.valueOf(500));
+
+ BigDecimal result = accountService.balanceOf("a0000001");
+ 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));
+ }
+
+ @Test
+ public void givenProgrammaticTx_whenRollback_thenAllRolledBack() throws Exception {
+ assertThatThrownBy(() -> {
+ tellerService.executeTransferProgrammaticTx("a0000002", "a0000001", BigDecimal.valueOf(100000));
+ }).hasMessage("Insufficient fund.");
+
+ assertThat(accountService.balanceOf("a0000001")).isEqualByComparingTo(BigDecimal.valueOf(1000));
+ assertThat(accountService.balanceOf("a0000002")).isEqualByComparingTo(BigDecimal.valueOf(2000));
+ assertThat(auditService.lastTransferLog()).isNull();
+ }
+}
diff --git a/pom.xml b/pom.xml
index b10cab6b1a..378d236b8e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -602,6 +602,7 @@
spring-reactive-kotlin
jnosql
spring-boot-angular-ecommerce
+ jta