# Handling Distributed Transactions in Java: 6 Proven Patterns Explained

> Master distributed transactions in Java with 6 proven patterns like XA, TCC, and Saga. Learn to balance consistency, availability, and complexity for robust systems.

- Repository: [Doocs/advanced-java](https://github.com/doocs/advanced-java)
- Tags: deep-dive
- Published: 2026-02-28

---

**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`](https://github.com/doocs/advanced-java/blob/main/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`](https://github.com/doocs/advanced-java/blob/main/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`](https://github.com/doocs/advanced-java/blob/main/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`](https://github.com/doocs/advanced-java/blob/main/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`](https://github.com/doocs/advanced-java/blob/main/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

```java
// pom.xml dependency
<dependency>
    <groupId>com.atomikos</groupId>
    <artifactId>transactions-jta</artifactId>
    <version>5.0.8</version>
</dependency>

```

```java
@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();
    }
}

```

```java
@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`](https://github.com/doocs/advanced-java/blob/main/docs/distributed-system/distributed-transaction.md): "两阶段提交… 基于 Spring + JTA 就可以搞定."

### TCC Skeleton with Spring Cloud

```java
// 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);
}

```

```java
@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`](https://github.com/doocs/advanced-java/blob/main/docs/distributed-system/distributed-transaction.md) (lines 40-45).

### Saga with Axon Framework

```java
@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`](https://github.com/doocs/advanced-java/blob/main/docs/distributed-system/distributed-transaction.md) outlines this pattern.

### Local Message Table (Outbox)

```java
// 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;
}

```

```java
@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
    }
}

```

```java
// 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`](https://github.com/doocs/advanced-java/blob/main/docs/distributed-system/distributed-transaction.md) details this "本地消息表" approach.

### Reliable Message (RocketMQ Transaction Message)

```java
// 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`](https://github.com/doocs/advanced-java/blob/main/docs/distributed-system/distributed-transaction.md).

### Max-Effort Notification

```java
@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`](https://github.com/doocs/advanced-java/blob/main/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`](https://github.com/doocs/advanced-java/blob/main/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.