TCC (Try-Confirm-Cancel) Distributed Transaction Pattern
1. Overview
A. Definition
TCC (Try-Confirm-Cancel) is a compensation-based distributed transaction pattern that decomposes a single distributed transaction into three operations per participating service — Try (reserve resources), Confirm (commit the reservation), and Cancel (release the reservation) — so that when every service's Try succeeds the Confirm is invoked, and when any one fails the Cancel is invoked, thereby implementing two-phase commit semantics at the application level.
TCC becomes clear when understood as a pattern that lifts the "prepare-commit" structure of 2PC up into business logic. Whereas 2PC relies on the locks and undo/redo logs of the resource manager (the DB) to hold resources, TCC has each service expose three idempotent APIs that reserve, confirm, and cancel its own resources, and a coordinator invokes these three operations in two phases (phase 1 Try, phase 2 Confirm/Cancel). For this reason TCC is often called "application-level 2PC" or a compensation-based transaction. Its intellectual roots lie in the tentative operation / reservation concepts Pat Helland presented in the CIDR 2007 paper Life beyond Distributed Transactions, and it became firmly established as a practical pattern through implementations such as Alibaba's Seata TCC mode.
B. Background and Necessity
Microservice architecture (MSA) follows the DB per Service principle, so one business operation (e.g., an e-commerce order = debit account + decrement inventory + accrue points) is performed across different services and different physical DBs. Atomicity can no longer be guaranteed with a single COMMIT as in the monolithic era, so a distributed transaction strategy becomes necessary. The traditional solution, Two-Phase Commit (2PC/XA), provides strong consistency, but after phase-1 prepare it holds DB locks for a long time until the commit completes, degrading throughput and availability, and it has a blocking problem in which participants stall while holding resources when the coordinator fails. This does not fit large-scale Internet and financial traffic.
The Saga gains availability by processing each step as an immediately committed local transaction and correcting failures with after-the-fact compensation, but it has the weakness that intermediate changes are immediately exposed externally (dirty reads) and there is no isolation. For example, if the payment service actually withdraws money and then the inventory step fails, the customer's account already shows the withdrawal until the refund (compensation) occurs.
TCC is a compromise between these two extremes. Its core idea is "do not commit immediately; reserve first." In phase-1 Try, instead of consuming the actual resource, it only isolates and freezes a portion (e.g., available inventory 100 → available 90 · frozen 10), and only after every participant's reservation succeeds does phase-2 Confirm actually commit the frozen amount. As a result, it secures a quasi-isolation that does not hold DB locks for long (versus 2PC) while suppressing exposure of intermediate state through resource freezing (versus Saga). In other words, TCC is a strategy that "logically isolates by placing a reservation state on business data, instead of physically locking resources."
2. Overall Structure and Operating Principles
A TCC system consists of a transaction coordinator (TM/TC) that directs the global transaction, and TCC participants (Resources) that each implement the three interfaces try/confirm/cancel. The coordinator invokes every participant's Try in phase 1 and, depending on the result, invokes Confirm for all or Cancel for all in phase 2.
flowchart TB
subgraph APP["Application (global transaction start)"]
TM["Transaction coordinator (TM/TC)"]
end
subgraph P1["Account service"]
A1["try: freeze withdrawal amount"]
A2["confirm: frozen → actual withdrawal"]
A3["cancel: release freeze"]
end
subgraph P2["Inventory service"]
B1["try: reserve stock (available↓ · frozen↑)"]
B2["confirm: reserved → actual deduction"]
B3["cancel: release reservation"]
end
subgraph P3["Points service"]
C1["try: reserve accrual"]
C2["confirm: confirm accrual"]
C3["cancel: cancel reservation"]
end
TM -- "Phase 1 Try" --> A1 & B1 & C1
TM -- "Phase 2 Confirm (if all succeed)" --> A2 & B2 & C2
TM -- "Phase 2 Cancel (if any fails)" --> A3 & B3 & C3
The essence of this structure is that each resource is itself responsible for its three state transitions. The coordinator guarantees only the order of the two-phase calls, without knowing the business meaning of "what to reserve, confirm, or cancel," while each service handles the actual data manipulation. Thanks to this separation of concerns, the coordinator can evolve as a generic framework and the resource logic can evolve independently per domain.
Viewing the normal and failure flows along the time axis looks as follows. If even one Try fails in phase 1, the coordinator immediately switches phase 2 to Cancel, and it proceeds to Confirm only when all succeed.
sequenceDiagram
participant TM as Coordinator (TC)
participant Acc as Account
participant Inv as Inventory
participant Pt as Points
Note over TM,Pt: Phase 1 — Try (reserve)
TM->>Acc: try (freeze 10,000 won withdrawal)
Acc-->>TM: OK
TM->>Inv: try (reserve 1 item)
Inv-->>TM: OK
TM->>Pt: try (reserve 100p accrual)
Pt-->>TM: FAIL (limit exceeded)
Note over TM,Pt: Phase 2 — any failure → Cancel all
TM->>Acc: cancel (release freeze)
TM->>Inv: cancel (release reservation)
TM->>Pt: cancel (empty rollback)
Note right of TM: had all Trys succeeded,<br/>confirm would be called instead
A. Phase 1 — Try (Reserve and Validate Resources)
Try is the step that performs a feasibility check plus resource isolation. It does not merely confirm "whether it can be done"; it freezes in advance the resources needed so that the subsequent Confirm is guaranteed to succeed. For example, instead of immediately deducting 10,000 won from the balance, the account service's Try only shifts the state as available balance -= 10,000, frozen balance += 10,000. This way, another transaction arriving concurrently cannot double-use the same 10,000 won, so isolation is secured, and since the actual withdrawal has not yet occurred, reverting (Cancel) is cheap. Therefore the quality of Try design determines the integrity of the entire TCC, and the core principle is to "validate and reserve sufficiently in Try so that Confirm cannot fail."
B. Phase 2 — Confirm (Commit the Reservation)
Confirm is the step that actually consumes the resources frozen in Try. For the account service it finalizes the withdrawal with frozen balance -= 10,000, and for the inventory service it deducts the reserved amount from real stock. An important design premise of Confirm is that it must never fail. Since all validation and reservation were completed in Try, Confirm performs no additional checks and only commits the reserved resource. If Confirm fails temporarily due to a network error, the coordinator retries until it succeeds (at-least-once), so Confirm must be idempotent. This premise that "Confirm is made to succeed by retrying" is a TCC-specific trait distinct from Saga's compensation.
C. Phase 2 — Cancel (Release the Reservation)
Cancel is the step that restores to the original state the frozen resources of participants that already succeeded, when even one Try failed in phase 1. The account's Cancel reverts with frozen balance -= 10,000, available balance += 10,000. Because Try only froze resources rather than actually consuming them, Cancel is simpler and safer than Saga's semantic compensation (a new forward transaction like a refund). Cancel too may be retried, so it must be idempotent.
3. TCC's Three Integrity Challenges (per Seata)
There are three abnormal situations that must be solved when actually implementing TCC, and Apache Seata's TCC design-principle document organizes them canonically. Failing to handle these three challenges leaves frozen resources locked forever or corrupts data.
| Challenge | Situation | Standard handling |
|---|---|---|
| Idempotence | Try/Confirm/Cancel is invoked two or more times due to network retries | Record transaction state in a log table and skip already-processed requests (result of one and many executions is identical) |
| Empty Rollback | Cancel is invoked first even though Try never arrived | Record whether Try ran; if there is no Try record, Cancel does nothing and returns success |
| Hanging | A delayed Try arrives later than Cancel, leaving the freeze permanently | Cancel leaves a log, and Try refuses to execute if that Cancel record exists |
Idempotence is needed because communication in distributed environments is inherently at-least-once. The coordinator re-invokes until it succeeds when it receives no response, but in reality the previous call often succeeded and only the response was lost. If Confirm is not idempotent in this case, the withdrawal is committed twice, causing a double deduction. In practice this is implemented by keeping a state field per transaction ID and "not re-confirming if the state is already CONFIRMED," or by checking the number of affected rows of an UPDATE and skipping if it is zero.
Empty rollback occurs when the coordinator does not receive the result of the Try call (the Try never reached the participant due to a network problem), decides to roll back the transaction, and invokes Cancel. From the participant's standpoint this is a request to cancel a resource it never reserved, so if there is no Try record, Cancel must be a no-op and return success. Otherwise, unconditionally restoring resources releases a non-existent freeze and produces the error of increasing the balance.
Hanging (suspension) is a follow-on risk of empty rollback. If a Try arrives belatedly after Cancel processing due to network delay, this Try freezes resources, but since the global transaction has already ended, the Confirm/Cancel that would release the freeze never comes again. As a result, frozen resources are locked permanently. The solution is for Cancel to leave in the log that "this transaction was canceled," and for the belated Try to check that record and refuse to execute on its own. Because these three challenges are intertwined, they are usually managed together with a single transaction-state log table.
4. Comparison with 2PC and Saga
All three patterns handle distributed transactions, but they differ fundamentally in consistency, isolation, and the way resources are held. Looking at "why that difference arises" rather than a simple list of items makes the selection criteria clear.
| Category | 2PC (XA) | TCC | Saga |
|---|---|---|---|
| Consistency | Strong consistency | Quasi-strong (effectively short-window eventual) | Eventual consistency |
| Isolation | Guaranteed by DB locks | Quasi-isolation via resource freezing | None (intermediate exposure) |
| Rollback method | DB undo log | Cancel (release reservation) | Semantic compensation transaction |
| Resource holding | Long (locked until commit) | Short (freeze only) | Almost none |
| Intrusiveness | Low (DB handles it) | High (implement 3 APIs) | Medium (compensation logic) |
| Suitable domain | Few, homogeneous DBs | Finance, payments, strong-consistency needs | Long-running, loosely coupled flows |
In 2PC, the DB resource manager handles isolation and recovery on behalf of the application with locks and logs, so application intrusiveness is low, but at the cost of holding locks for a long time, which scales poorly. TCC shifts the isolation and recovery responsibility to the application and reduces lock holding to the freeze level, but because every participating service must directly implement three APIs, its development cost and intrusiveness are high. Saga abandons isolation altogether, but in exchange its implementation is the loosest and it suits long-running flows, yet intermediate state is exposed, making it unsuitable for financial transactions where isolation matters. Thus the typical application domain of TCC is areas such as payment, account transfer, and inventory deduction that require "strong consistency and short resource holding at the same time," while Saga better fits loose workflows (an entire travel-booking flow, etc.).
5. Deep Dive — Practical Application and Framework Trends
TCC is especially widely used in high-traffic financial transactions at large Chinese Internet companies. Representatively, Alibaba/Ant-family payment and account systems process tens of thousands of transfers per second and could not tolerate 2PC's lock holding, so they adopted resource-freezing-based TCC as a core pattern. The open-sourced version of this experience is the TCC mode of Apache Seata: developers declare the Try method with @TwoPhaseBusinessAction and specify the commit/rollback method names, and then Seata's transaction coordinator (TC) manages the global transaction log and supports two-phase invocation, retries, and defenses against idempotence/empty-rollback/hanging. In addition, Java-ecosystem TCC frameworks such as Hmily, ByteTCC, and TCC-Transaction provide similar models.
As a concrete example, consider a bank account transfer: the Try of the withdrawing account freezes the transfer amount, and the Try of the depositing account creates a "pending deposit" record. When both Trys succeed, in Confirm the withdrawing account actually deducts the frozen amount and the depositing account reflects the pending amount in the real balance. If either fails, Cancel reverts the frozen/pending records. In this process, the available balance the customer sees decreases immediately at Try, but since the actual accounting withdrawal is committed at Confirm, double use is fundamentally blocked. In another example, airline ticket booking, the seat Try reserves a specific seat in "HOLD" status for a few minutes, and after the payment Try succeeds, Confirm finalizes it as "CONFIRMED" — the familiar "pay within N minutes" seat-hold UX is precisely aligned with TCC's resource-freezing model.
As for recent trends, to reduce the high implementation cost of pure TCC (every service writing triple the APIs), Seata provides, alongside TCC, an AT (Automatic Transaction) mode (a DB proxy automatically generates undo logs to minimize developer work), and designs that mix the TCC/AT/Saga/XA modes depending on the situation are spreading. Moreover, hybrid transaction designs that combine message-based reliable events (Transactional Outbox) with TCC — handling only the critical steps that truly require strong consistency with TCC and the rest loosely with events — are becoming mainstream in practice.
6. Considerations and Implications (Professional Engineer's Perspective)
Adopting TCC is not merely a library choice but entails trade-offs across the data model, operations, and the organization, so the following must be judged strategically.
Selecting the scope of application (application strategy): Wrapping every transaction in TCC explodes development and operational costs. It is realistic to apply it selectively only to core operations that simultaneously require strong isolation and short holding (payment, transfer, inventory), and to separate loose flows into Saga/events in a mixed strategy. The key is the design capability to "differentially apply patterns according to the level of consistency required."
Implementation intrusiveness and design difficulty (trade-off): TCC requires each participating service to implement three operations — Try, Confirm, and Cancel — plus idempotence, empty-rollback, and hanging defenses, so per-service code volume and testing burden increase significantly. In particular, to make Confirm "never fail," sufficient reservation in Try is a prerequisite, so an intrusive design that adds freeze/reservation state fields to the domain data model is unavoidable. Application is limited when three APIs cannot be forced, as with legacy or external services.
Isolation side effects and observability (operations): TCC's quasi-isolation presumes a short "resource freeze" window, so if the time from Try to Confirm lengthens, frozen resources pile up and can cause capacity exhaustion or deadlock. Since unfinished transactions remaining due to coordinator failure or participant non-response leave freezes unresolved, operational observability is essential: global transaction log monitoring, timeout-based automatic Cancel, and a console for manual intervention on pending transactions.
Data integrity verification and remediation (outlook, related technologies): Because correctness must be upheld with idempotency keys and state logs over at-least-once communication, a safety net of periodic reconciliation batches that cross-verify freeze-confirm states must be in place. Related technologies such as Saga, Transactional Outbox, and CDC combine naturally, and amid the multi-mode evolution of transaction-mesh frameworks (Seata, etc.) and the spread of serverless/event architectures, TCC is expected to settle as a "precision tool for the points that require strong consistency."
References
- Apache Seata, "TCC Mode Design Principle", https://seata.apache.org/blog/tcc-mode-design-principle
- Apache Seata, "TCC Mode" User Docs, https://seata.apache.org/docs/user/mode/tcc/
- Pat Helland, "Life beyond Distributed Transactions: an Apostate's Opinion", CIDR 2007, https://ics.uci.edu/~cs223/papers/cidr07p15.pdf
In one line: TCC decomposes a distributed transaction into per-service Try (reserve), Confirm (commit), and Cancel (release) operations to obtain, without 2PC's long lock holding, a quasi-isolation stronger than Saga's — an application-level two-phase commit pattern whose success hinges on idempotence, empty-rollback, and hanging defenses.