Introduction

In a modern application that interacts with different database and app services, handling the distributed transactions are very efficient and crucial. To manage the data consistency across the multiple resources XA transaction(eXtended Architecture) is a standard way.

In this article, we will explore how to use the XA Transactions in Java using Hazelcast data grid.

XA Transactions in Java using Hazelcast
XA Transactions in Java using Hazelcast

What are XA Transactions?

XA Transactions are defined by the X/Open Group, it is a protocol that enables features to manage the transactions across multiple resource managers (like databases, message queues, etc.). Transactions are managed and co-ordinates by the transaction manager(TM). XA Transactions are integrated with multiple platforms and frameworks.

Why Use XA Transactions?

  • If you need to achieve consistency across the multiple databases, XA Transaction ensures to manage the transaction.
  • XA Transactions support distributed transaction management, for example – Hazelcast and database storage transaction management.
  • It also prevents partial commits in case of failures.

Understanding Two-Phase Commit (2PC)

XA Transactions in Java using Hazelcast in-memory data grid, rely on two-phase commits protocol:

  • Phase1 – Prepare: The Transaction Manager manages all resources and it asks the resources to prepare.
  • Phase2 – Commit/Rollback: If the execution process succeeds in all resources, then the transaction gets committed, otherwise it gets rolled back.

How Hazelcast Supports XA Transactions

XA Transactions in Java using Hazelcast has in-built HazelcastInstance and XA resources, that support XA Transaction feature, using using any third party library like <groupId>com.atomikos</groupId> transaction management. This acts as a cache data store and it also participates in XA transactions with RDBMS.

Setting Up Hazelcast for XA Transactions

To understand the implementation of XA Transactions in Java using Hazelcast, here is the code example we are demonstrating user, which are persisted into the Hazelcast Map and Persistence database.

To setup the Hazelcast environment, you can refer the following articles.

Add the Required Dependencies

Add the maven dependencies for Hazelcast and Transaction Management

<dependencies>
    <dependency>
        <groupId>com.hazelcast</groupId>
        <artifactId>hazelcast</artifactId>
        <version>5.3.0</version>
    </dependency>
    <dependency>
        <groupId>com.hazelcast</groupId>
        <artifactId>hazelcast-xa</artifactId>
        <version>5.3.0</version>
    </dependency>
    <dependency>
        <groupId>com.zaxxer</groupId>
        <artifactId>HikariCP</artifactId>
        <version>5.0.1</version>
    </dependency>
    <dependency>
        <groupId>org.h2database</groupId>
        <artifactId>h2</artifactId>
        <version>2.1.214</version>
    </dependency>
</dependencies>

XA Transaction with Database

The following are a code snippet where we are demonstrating the XATransaction, where data is written into Hazelcast in the Database.

HazelcastXADemoExample.java
package com.javatecharc.demo.hazelcast.transaction;

import com.hazelcast.config.Config;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.transaction.TransactionContext;
import com.hazelcast.transaction.TransactionalMap;
import com.hazelcast.xa.HazelcastXAResource;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

import javax.sql.DataSource;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import java.sql.Connection;
import java.sql.PreparedStatement;

public class HazelcastXADemoExample {
    public static void main(String[] args) throws Exception {
        // Initialize Hazelcast instance
        Config config = new Config();
        HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);

        // Obtain XA Resource from Hazelcast
        HazelcastXAResource xaResource = hazelcastInstance.getXAResource();

        // Configure Database (H2 + HikariCP)
        DataSource dataSource = getDataSource();
        Connection connection = dataSource.getConnection();
        connection.setAutoCommit(false);

        // Create a unique transaction ID (Xid)
        Xid xid = new SampleXid(123, new byte[]{0x01}, new byte[]{0x02});

        // Start XA Transaction
        xaResource.start(xid, XAResource.TMNOFLAGS);

        // Begin Hazelcast Transaction
        TransactionContext context = hazelcastInstance.newTransactionContext();
        context.beginTransaction();

        // Hazelcast Operation
        TransactionalMap<Integer, String> txMap = context.getMap("JavaTechARCXAMap");
        txMap.put(1, "Hazelcast XA Transaction");

        // Database Operation
        PreparedStatement stmt = connection.prepareStatement("INSERT INTO users (id, name) VALUES (?, ?)");
        stmt.setInt(1, 1);
        stmt.setString(2, "XA User");
        stmt.executeUpdate();

        // Prepare phase for two-phase commit
        xaResource.end(xid, XAResource.TMSUCCESS);
        int prepare = xaResource.prepare(xid);

        if (prepare == XAResource.XA_OK) {
            // Commit Hazelcast Transaction
            xaResource.commit(xid, false);
            context.commitTransaction();

            // Commit Database Transaction
            connection.commit();
            System.out.println("XA Transaction committed successfully!");
        } else {
            // Rollback if any issue
            xaResource.rollback(xid);
            context.rollbackTransaction();
            connection.rollback();
            System.out.println("XA Transaction rolled back!");
        }

        // Cleanup
        connection.close();
        hazelcastInstance.shutdown();
    }

    // Database Configuration using HikariCP
    private static DataSource getDataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1");
        config.setUsername("sa");
        config.setPassword("");
        return new HikariDataSource(config);
    }
}

Sample XId class implementation

An implementation of XId class where XA Transaction require identification.

SampleXid.java
package com.javatecharc.demo.hazelcast.transaction;

import javax.transaction.xa.Xid;
import java.util.Arrays;

public class SampleXid implements Xid {
    private final int formatId;
    private final byte[] globalTransactionId;
    private final byte[] branchQualifier;

    public SampleXid(int formatId, byte[] globalTransactionId, byte[] branchQualifier) {
        this.formatId = formatId;
        this.globalTransactionId = globalTransactionId;
        this.branchQualifier = branchQualifier;
    }

    @Override
    public int getFormatId() {
        return formatId;
    }

    @Override
    public byte[] getGlobalTransactionId() {
        return globalTransactionId;
    }

    @Override
    public byte[] getBranchQualifier() {
        return branchQualifier;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        SampleXid that = (SampleXid) obj;
        return formatId == that.formatId &&
                Arrays.equals(globalTransactionId, that.globalTransactionId) &&
                Arrays.equals(branchQualifier, that.branchQualifier);
    }

    @Override
    public int hashCode() {
        return Arrays.hashCode(globalTransactionId) + Arrays.hashCode(branchQualifier);
    }
}

The Code Explanation

  1. Hazelcast Instance: Create Hazelcast instance from distributed clusters.
  2. XA Resource: Create XAResource for the database connection and data storage
  3. XA Transaction (Two-Phase Commit)
    • Start the XA transaction.
    • Perform a transactional write operation on Hazelcast.
    • Insert a record in the H2 database.
    • Call prepare() to check if all operations can commit.
    • If successful, commit both Hazelcast and database operations.
    • If any error occurs, rollback everything.
  4. Xid Implementation: Create unique identified for transaction

Output (Successful Transaction)

On successful transaction print the message:

XA Transaction committed successfully!

Output (Rollback Scenario)

If any error then rollback the transaction

XA Transaction rolled back!

Performance Considerations

  • XA Transactions introduce may use more memory due to 2PC.
  • Avoid XA with single resources
  • Use HikariCP or similar connection pooling to improve efficiency.

Best Practices for XA Transactions

  • Prefer local transactions where it is possible.
  • Use JTA-compliant transaction managers, e.g. atomikos.
  • Try to avoid lock the resources
  • Implement and monitor the logs

Conclusion

XA Transactions in Java using Hazelcast provide a way to manage the distributed transactions across multiple resources. Java developer can build a reliable and consistent application by using the these capabilities

   

 ✅ XA Transactions allow atomic operations across the resources, like Hazelcast and multiple database

 ✅ HazelcastXAResource ensures Hazelcast should be part of the transaction and resource management.

 ✅ HikariCP with H2 manages database transactions and persistence efficiently.

✅ Two-Phase Commit (2PC) manages the transactions.

More detailed implementation of the Java code example is available on GitHub.