Choosing the Right Distributed Transaction Strategy for Microservices

The article reviews the most common distributed transaction patterns—TCC, Saga, reliable messaging, 2PC, and Seata's AT mode—explaining their principles, ideal use‑cases, implementation details, and trade‑offs so architects can select the most suitable solution for their microservice systems.

Java Baker
Java Baker
Java Baker
Choosing the Right Distributed Transaction Strategy for Microservices

The author, a Java specialist, introduces the challenge of maintaining data consistency across services in a microservice architecture, where a single business operation may involve multiple databases or external RPC calls.

BASE Theory

In a single‑database transaction strong consistency is guaranteed, but in distributed scenarios only eventual consistency can be achieved. The BASE model (Basically Available, Soft state, Eventual consistency) guides most distributed transaction solutions, which sacrifice strong consistency for availability.

Main Solutions and Ideal Scenarios

1. TCC

Best scenario: high‑concurrency operations such as order placement that must lock inventory and balance simultaneously.

Principle: a Transaction Manager (TM) coordinates Try, Confirm, and Cancel phases; each service implements a Resource Manager (RM) exposing these three interfaces and ensures idempotency.

Process:

TM calls Try on each service to reserve resources (e.g., freeze inventory, freeze balance) without acquiring DB row locks.

If all Try calls succeed, TM calls Confirm to make the reservations permanent.

If any Try fails or times out, TM calls Cancel on all previously successful services to release reservations.

The framework must handle empty rollbacks, hanging Cancel calls, and idempotency by recording a global transaction status and querying it via a global transaction ID.

Example code for the inventory service’s Try/Confirm/Cancel methods is shown below.

tcc
tcc
// Try phase: freeze inventory
@Transactional
public boolean tryFreezeInventory(String productId, int qtyDelta, String txId) {
    // update inventory set frozen_qty = frozen_qty + #{qtyDelta}, available_qty = available_qty - #{qtyDelta}
    // where product_id = #{productId} and available_qty - #{qtyDelta} > 0
    if (inventoryMapper.freeze(productId, qtyDelta) > 0) {
        freezeRecordMapper.insert(txId, productId, qtyDelta);
        return true;
    } else {
        return false;
    }
}

// Confirm phase: deduct frozen inventory
@Transactional
public void confirm(String productId, int qtyDelta, String txId) {
    inventoryMapper.deduct(productId, qtyDelta);
    freezeRecordMapper.updateStatus(txId, "CONFIRMED");
}

// Cancel phase: release frozen inventory
@Transactional
public void cancel(String productId, int qtyDelta, String txId) {
    FreezeRecord r = freezeRecordMapper.selectByTxId(txId);
    if (r == null) return; // empty rollback
    inventoryMapper.release(productId, qtyDelta);
    freezeRecordMapper.updateStatus(txId, "CANCELLED");
}

TCC turns all resource operations into reservations, avoiding long‑held DB locks, making it ideal for flash‑sale or payment scenarios with intense concurrency.

2. Saga

Best scenario: long‑running, multi‑service workflows such as travel booking (flight + hotel + tickets) that involve external third‑party calls.

Principle: the overall transaction is split into ordered local transactions; each step commits immediately, and if a later step fails, previously successful steps are compensated in reverse order.

Implementation: an orchestrator (central coordinator) invokes each local transaction and, on failure, triggers the corresponding compensation actions. The orchestrator persists the Saga state to survive crashes.

Key points:

Every forward transaction must have an idempotent compensation transaction.

Saga does not lock resources, so intermediate states may be visible (temporary dirty reads).

Compensation failures require alerting and possible manual intervention.

Example code for a hotel reservation and its compensation is provided.

saga
saga
// Forward transaction: reserve hotel
public Booking bookHotel(HotelReq req) {
    String bookingId = hotelApi.reserve(req);
    return bookingRepo.save(new Booking(bookingId, "RESERVED"));
}

// Compensation transaction: cancel hotel reservation
public void cancelHotel(Booking booking) {
    hotelApi.cancel(booking.getBookingId());
    bookingRepo.updateStatus(booking.getId(), "CANCELLED");
}

Saga avoids database locks and keeps services independent, but requires well‑designed compensation logic and tolerance for short periods of inconsistency.

3. Reliable Messaging

Best scenario: asynchronous, non‑core processes such as awarding points after user registration, where eventual delivery is required and duplicate messages can be handled idempotently.

Principle: the local‑outbox pattern combines the business operation and a pending message in the same DB transaction; a background task later publishes pending messages to a message queue and updates their status.

Key steps:

Message producer inserts a "PENDING" record into an outbox table together with the business data.

A scheduled task scans for stale pending records and attempts to send them to the MQ, marking them "SENT" on success.

Message consumer processes the message and ensures idempotency via a unique business key.

SQL example of inserting a user and an outbox record is shown.

msg
msg
BEGIN;
INSERT INTO users(user_id, email, ...) VALUES (...);
INSERT INTO outbox(message_id, topic, payload, status)
VALUES ('msg_reg_123', 'USER_REGISTERED', '{"userId":...}', 'PENDING');
COMMIT;

This approach decouples services while guaranteeing eventual consistency, provided downstream consumers handle duplicates.

4. Two‑Phase Commit (2PC)

Best scenario: low‑concurrency, strong‑consistency requirements such as internal financial transfers between accounts stored in separate databases.

Principle: a global coordinator orchestrates a prepare (vote) phase and a commit/rollback phase. Participants lock resources and write undo/redo logs during the prepare phase.

Process details:

Phase 1 – Prepare: coordinator asks each participant if it can commit; participants lock rows, execute their part of the transaction, and respond Yes/No.

Phase 2 – Commit/Rollback: if all say Yes, the coordinator sends a commit command; otherwise it sends a rollback. Participants act accordingly and release locks.

Failure mode: coordinator single‑point‑of‑failure can leave participants in an uncertain state, requiring manual intervention or log‑based recovery.

Typical implementation uses the XA protocol (e.g., Java JTA), which handles the two phases automatically.

2pc
2pc

2PC provides strong consistency but incurs high latency and low throughput due to resource locking.

5. AT Mode (Seata)

Best scenario: retrofitting legacy monoliths split into microservices where minimal code changes are desired.

Principle: Seata adds a proxy layer that records an undo log for each DB operation within a local transaction, effectively implementing a non‑intrusive 2PC.

Phase 1 – In the same local transaction, business data and undo log are persisted; a global lock is acquired.

Phase 2 – After successful commit, the undo log is asynchronously deleted; rollback uses the undo log to reverse changes.

Example: an inventory update SQL and the automatically generated undo SQL are shown.

update inventory set qty = qty - 1 WHERE product_id = 1001;
-- Seata records undo log: before qty=100, after qty=99
update inventory set qty = 100 WHERE product_id = 1001; -- rollback SQL

AT mode offers low code intrusion but relies on relational databases, defaults to read‑uncommitted isolation, and may not suit complex queries or high‑performance requirements.

Solution Comparison Summary

TCC – high concurrency, strong or eventual consistency, high performance, high code intrusion.

Saga – long workflows with external calls, eventual consistency, high performance, medium code intrusion.

Reliable Messaging – asynchronous non‑core processes, eventual consistency, high performance, medium code intrusion.

2PC – internal low‑concurrency transfers, strong consistency, low performance, low code intrusion.

AT (Seata) – legacy system refactoring with minimal changes, weak consistency/read‑uncommitted, medium performance, low code intrusion.

When selecting a distributed transaction strategy, consider how long data inconsistency can be tolerated, the expected concurrency level, and whether compensation logic can be implemented.

By matching the workload characteristics with the table above, architects can choose the most appropriate pattern and implement it with Apache Seata for TCC, Saga, and AT, or with a local outbox table for reliable messaging.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

microservices2PCTCCdistributed transactionsSagaSeatareliable messaging
Java Baker
Written by

Java Baker

Java architect and Raspberry Pi enthusiast, dedicated to writing high-quality technical articles; the same name is used across major platforms.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.