Handling Distributed Transactions in Java: 6 Proven Patterns Explained

Distributed transactions in Java are best handled using one of six patterns—XA Two-Phase Commit, TCC, Saga, Local Message Table, Reliable Message, or Max-Effort Notification—each balancing consistency, availability, and complexity according to the CAP theorem.

Handling distributed transactions in Java becomes essential when a business operation spans multiple autonomous services or databases. The doocs/advanced-java repository documents six mainstream approaches in docs/distributed-system/distributed-transaction.md, each with distinct trade-offs and typical usage scenarios. This guide examines each pattern with implementation details and code examples derived directly from the source analysis.

Understanding Distributed Transaction Challenges

Distributed systems face the fundamental tension described by the CAP theorem: you cannot simultaneously guarantee Consistency, Availability, and Partition tolerance. When handling distributed transactions in Java, architects must choose between strong consistency (sacrificing availability) and eventual consistency (maximizing availability). The six patterns below represent different positions on this spectrum.

Six Patterns for Handling Distributed Transactions in Java

XA Two-Phase Commit (2PC)

The XA protocol implements a global transaction manager that coordinates prepare and commit phases across all participants. In docs/distributed-system/distributed-transaction.md, this is described as the strictest approach for handling distributed transactions in Java when ACID guarantees are non-negotiable.

Key Characteristics:

  • Strong consistency across multiple databases
  • Low throughput and high latency due to locking
  • Typically implemented with Spring + JTA (Atomikos)

When to Use: Small-scale systems requiring strict ACID guarantees across a few relational databases.

TCC (Try-Confirm-Cancel)

TCC is an explicit three-phase pattern where each service implements Try (reserve resources), Confirm (commit), and Cancel (compensate) operations. According to the repository's analysis, this approach demands strong consistency for critical resources like payment processing.

Key Characteristics:

  • Hand-written compensation logic required
  • High reliability but complex maintenance
  • Resource reservation prevents dirty reads during the transaction

When to Use: Scenarios demanding strong consistency for critical resources (e.g., payment, order processing).

Saga Pattern

The Saga pattern manages long-running business processes by having each service commit its local transaction and publish an event. If a subsequent step fails, compensating actions roll back earlier steps. The docs/distributed-system/distributed-transaction.md file positions this as the preferred approach for high-concurrency environments where eventual consistency is acceptable.

Key Characteristics:

  • High concurrency with no distributed locks
  • Compensation logic required for rollback scenarios
  • Can be orchestrated (central coordinator) or choreographed (event-driven)

When to Use: Long-running business processes where eventual consistency is acceptable (e.g., travel booking, multi-step order workflows).

Local Message Table (Outbox)

The Local Message Table pattern (also known as Outbox) writes events to a local "outbox" table within the same database transaction as the business operation. A separate process polls this table and publishes events to a message broker. The repository notes this approach in docs/distributed-system/distributed-transaction.md as suitable for systems already using relational databases.

Key Characteristics:

  • Relies heavily on the database as a durable queue
  • Guarantees message delivery without a dedicated transaction coordinator
  • May hinder scalability under extreme load

When to Use: Systems that already use relational databases and need reliable eventual consistency without a dedicated broker.

Reliable Message (Transactional MQ)

Reliable Message leverages transactional messaging capabilities (e.g., RocketMQ, Apache Kafka with exactly-once semantics) where the producer sends a prepared message to the broker, executes the local database transaction, and then commits or rolls back the message based on the outcome. The docs/distributed-system/distributed-transaction.md describes this as decoupling the transaction from the database.

Key Characteristics:

  • Decouples transaction management from database operations
  • Broker handles retries and delivery guarantees
  • Requires messaging middleware that supports transactional messages

When to Use: Environments that support transactional messaging (e.g., RocketMQ, Pulsar) where you want to eliminate the outbox table.

Max-Effort Notification

Max-Effort Notification represents the weakest consistency guarantee. After committing locally, a worker repeatedly attempts to notify downstream services via message queue, giving up after a configurable number of retries. According to the repository analysis, this suits low-risk operations where occasional manual intervention is acceptable.

Key Characteristics:

  • Simple to implement with minimal overhead
  • No guaranteed delivery (best-effort only)
  • May require manual remediation for unrecoverable failures

When to Use: Low-risk operations where occasional manual intervention is acceptable (e.g., cache warm-up, analytics).

Implementation Examples

Below are concise, runnable snippets illustrating each pattern using common Java libraries. These are derived from the source analysis of doocs/advanced-java and are illustrative only—adapt them to your project's stack.

XA Transaction with Spring + Atomikos

// pom.xml dependency
<dependency>
    <groupId>com.atomikos</groupId>
    <artifactId>transactions-jta</artifactId>
    <version>5.0.8</version>
</dependency>
@Configuration
@EnableTransactionManagement
public class XaConfig {

    @Bean(initMethod = "init", destroyMethod = "close")
    public DataSource xaDataSource() {
        AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
        ds.setXaDataSourceClassName("com.mysql.cj.jdbc.MysqlXADataSource");
        Properties p = new Properties();
        p.setProperty("url", "jdbc:mysql://db1:3306/db1");
        p.setProperty("user", "root");
        p.setProperty("password", "pwd");
        ds.setXaProperties(p);
        return ds;
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        return new JtaTransactionManager();
    }
}
@Service
public class OrderService {

    @Transactional
    public void createOrder(Order order) {
        orderRepo.save(order);          // DB1
        inventoryService.reserve(order); // DB2 – same global transaction
    }
}

This example follows the XA description in docs/distributed-system/distributed-transaction.md: "两阶段提交… 基于 Spring + JTA 就可以搞定."

TCC Skeleton with Spring Cloud

// TCC interface
public interface PaymentTccService {
    // Try – reserve funds
    boolean tryReserve(Long accountId, BigDecimal amount);

    // Confirm – deduct funds
    void confirm(Long accountId, BigDecimal amount);

    // Cancel – release reservation
    void cancel(Long accountId, BigDecimal amount);
}
@Service
public class PaymentTccServiceImpl implements PaymentTccService {

    @Autowired private AccountRepository repo;

    @Override
    public boolean tryReserve(Long accountId, BigDecimal amount) {
        // lock row & check balance
        return repo.lockAndCheck(accountId, amount);
    }

    @Override
    public void confirm(Long accountId, BigDecimal amount) {
        repo.decreaseBalance(accountId, amount);
    }

    @Override
    public void cancel(Long accountId, BigDecimal amount) {
        repo.releaseLock(accountId);
    }
}

The three-phase flow (Try-Confirm-Cancel) mirrors the TCC explanation in docs/distributed-system/distributed-transaction.md (lines 40-45).

Saga with Axon Framework

@Aggregate
public class OrderAggregate {

    @AggregateIdentifier
    private String orderId;
    private OrderStatus status;

    @CommandHandler
    public OrderAggregate(CreateOrderCmd cmd) {
        apply(new OrderCreatedEvent(cmd.getOrderId(), cmd.getAmount()));
    }

    @EventSourcingHandler
    public void on(OrderCreatedEvent evt) {
        this.orderId = evt.getOrderId();
        this.status = OrderStatus.CREATED;
    }

    // Compensation command
    @CommandHandler
    public void handle(CancelOrderCmd cmd) {
        apply(new OrderCancelledEvent(orderId));
    }
}

A Saga orchestrates a series of local transactions, each emitting events. The documentation's "Saga 方案" section (lines 56-85) in docs/distributed-system/distributed-transaction.md outlines this pattern.

Local Message Table (Outbox)

// Order table + outbox
@Entity
public class Order {
    @Id private Long id;
    private String status;
    // …
}

@Entity
public class OutboxEvent {
    @Id @GeneratedValue private Long id;
    private String topic;
    private String payload;
    private boolean sent = false;
}
@Service
@Transactional
public class OrderService {

    public void placeOrder(Order order) {
        orderRepo.save(order);
        OutboxEvent ev = new OutboxEvent("order.created", toJson(order));
        outboxRepo.save(ev); // same DB transaction
    }
}
// Separate worker (could be a Spring @Scheduled task)
@Scheduled(fixedDelay = 5000)
public void dispatchOutbox() {
    List<OutboxEvent> pending = outboxRepo.findBySentFalse();
    for (OutboxEvent ev : pending) {
        mqTemplate.send(ev.getTopic(), ev.getPayload());
        ev.setSent(true);
        outboxRepo.save(ev);
    }
}

The step-by-step description (lines 93-99) in docs/distributed-system/distributed-transaction.md details this "本地消息表" approach.

Reliable Message (RocketMQ Transaction Message)

// Producer
TransactionalMQProducer producer = new TransactionalMQProducer("order-producer");
producer.setTransactionListener(new TransactionListener() {
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        // 1️⃣ local DB transaction
        boolean success = orderService.save((Order) arg);
        return success ? LocalTransactionState.COMMIT_MESSAGE
                       : LocalTransactionState.ROLLBACK_MESSAGE;
    }

    @Override
    public LocalTransactionState checkLocalTransaction(Message msg) {
        // 2️⃣ broker callback – verify DB status
        return orderService.exists(msg.getTransactionId())
               ? LocalTransactionState.COMMIT_MESSAGE
               : LocalTransactionState.ROLLBACK_MESSAGE;
    }
});
producer.start();

This aligns with the "可靠消息最终一致性方案" (lines 106-115) described in docs/distributed-system/distributed-transaction.md.

Max-Effort Notification

@Service
public class NotificationWorker {

    private final RestTemplate rest = new RestTemplate();
    private final int MAX_RETRY = 5;

    public void notifyBSystem(String payload) {
        int attempts = 0;
        while (attempts < MAX_RETRY) {
            try {
                rest.postForObject("http://b-system/api/handle", payload, Void.class);
                break; // success
            } catch (Exception e) {
                attempts++;
                Thread.sleep(2000L); // back-off
            }
        }
        // after MAX_RETRY you may log/alert for manual handling
    }
}

The workflow matches the "最大努力通知方案" (lines 119-125) in docs/distributed-system/distributed-transaction.md.

Choosing the Right Pattern for Your Architecture

When handling distributed transactions in Java, align your choice with business criticality and consistency requirements:

  • Prefer Local Transactions + Asynchronous Consistency for most microservices. Modern architectures favor eventual consistency to achieve high availability per the CAP theorem. The Local Message Table and Reliable Message patterns let each service maintain its own ACID transaction while guaranteeing eventual propagation.

  • Reserve Strong Consistency for Critical Paths. Use TCC or XA only for operations where incorrect state is intolerable, such as monetary transfers. For TCC, ensure compensation code is small, idempotent, and thoroughly tested.

  • Leverage Saga for Long-Running Workflows. When a business process spans many services and may last minutes or hours, a Saga orchestrator manages the flow while keeping each service's transaction short and independent.

  • Choose Middleware-Appropriate Solutions. If your infrastructure supports transactional messaging (RocketMQ, Apache Pulsar), the Reliable Message approach eliminates the need for separate outbox tables and simplifies operational overhead.

  • Fallback to Max-Effort for Non-Critical Cases. For low-value data synchronization such as cache warm-up or analytics, the Max-Effort Notification pattern provides pragmatic, low-overhead eventual consistency.

Summary

Handling distributed transactions in Java requires selecting from six distinct patterns documented in the doocs/advanced-java repository:

  • XA Two-Phase Commit provides strict ACID compliance but sacrifices performance and availability.
  • TCC offers strong consistency for critical operations through explicit resource reservation and compensation.
  • Saga manages long-running workflows with eventual consistency and high concurrency.
  • Local Message Table ensures reliable event publishing using database-backed outbox tables.
  • Reliable Message leverages transactional message brokers like RocketMQ to decouple transaction state from business logic.
  • Max-Effort Notification provides lightweight, retry-based consistency for non-critical operations.

Each approach trades off between consistency, availability, and complexity as defined in docs/distributed-system/distributed-transaction.md.

Frequently Asked Questions

What is the best way to handle distributed transactions in Java microservices?

The best approach depends on your consistency requirements. For most microservices, asynchronous patterns like Local Message Table or Reliable Message provide the best balance of availability and data integrity. Reserve TCC or XA only for financial or critical paths where strong consistency is mandatory. The doocs/advanced-java repository recommends avoiding 2PC in high-throughput scenarios due to its blocking nature.

How does the Saga pattern differ from Two-Phase Commit?

Saga differs fundamentally from Two-Phase Commit (2PC) in its consistency model and locking behavior. While 2PC uses a global coordinator to lock resources until all participants commit, Saga allows each service to commit its local transaction immediately and uses compensating transactions to undo work if subsequent steps fail. This makes Saga non-blocking and highly available, but it provides eventual consistency rather than immediate ACID guarantees.

When should I use the Local Message Table pattern?

Use the Local Message Table (Outbox) pattern when your system already relies on relational databases and you need reliable eventual consistency without introducing complex transaction coordinators. This pattern works best when you can tolerate a short delay between the database commit and message delivery, and when you have infrastructure to run the polling worker that dispatches events from the outbox table to your message broker.

Is XA Two-Phase Commit suitable for high-performance systems?

No, XA Two-Phase Commit is generally unsuitable for high-performance systems due to its blocking nature and network overhead. The protocol requires all participants to lock resources during the prepare phase and wait for the coordinator's final decision, creating a single point of failure and reducing throughput. For high-performance scenarios, the doocs/advanced-java documentation recommends TCC, Saga, or asynchronous messaging patterns instead.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →