Transaction Management Patterns in Microservices: Blocking Retry, Async Queue, TCC, and Local Message Table

The article explains common microservice transaction patterns—including blocking retry, asynchronous queues, TCC compensation transactions, and local message tables—detailing their implementations, advantages, drawbacks, and practical code examples for ensuring data consistency in distributed systems.

Architect's Tech Stack
Architect's Tech Stack
Architect's Tech Stack
Transaction Management Patterns in Microservices: Blocking Retry, Async Queue, TCC, and Local Message Table

In modern distributed systems and microservice architectures, service-to-service call failures are common, making exception handling and data consistency critical.

Blocking Retry

Blocking retry is a typical approach where a request is retried a limited number of times before giving up. The following pseudocode demonstrates the idea:

m := db.Insert(sql)

err := request("B-Service", m)

func request(url string, body interface{}) {
    for i := 0; i < 3; i++ {
        result, err = request.POST(url, body)
        if err == nil {
            break
        } else {
            log.Print()
        }
    }
}

This method can cause duplicate data when the downstream service succeeds but the caller times out, generate dirty data if the request fails after a DB insert, and increase latency, putting extra pressure on downstream services.

Asynchronous Queue

Introducing a message queue decouples processing and improves reliability. After writing to the database, a message is published to the queue for an independent consumer to handle the business logic:

m := db.Insert(sql)

err := mq.Publish("B-Service-topic", m)

Although queues are more stable than direct service calls, publishing can still fail (e.g., network issues), leading to the same inconsistency problem where the DB write succeeds but the message is not delivered.

TCC Compensation Transaction

TCC (Try‑Confirm‑Cancel) splits each service call into three phases to achieve atomicity across services:

Try: check resources and reserve them (e.g., inventory lock).

Confirm: commit the reservation.

Cancel: release the reservation if Try fails.

Example pseudocode for a shopping scenario that calls inventory, amount, and points services:

m := db.Insert(sql)
aResult, aErr := A.Try(m)
bResult, bErr := B.Try(m)
cResult, cErr := C.Try(m)
if cErr != nil {
    A.Cancel()
    B.Cancel()
    C.Cancel()
} else {
    A.Confirm()
    B.Confirm()
    C.Confirm()
}

Key issues include empty releases (when a failed Try actually succeeded), ordering problems (Cancel arriving before Try), and failures during Confirm/Cancel, which may require additional retry or manual intervention.

Local Message Table

The local message table, originally proposed by eBay, stores a message record in the same database transaction as the business data, guaranteeing that either both succeed or both fail. After the main transaction commits, the message is sent to a queue; if the send fails, the message remains with a try status and is retried asynchronously.

messageTx := tc.NewTransaction("order")
messageTxSql := tx.TryPlan("content")

m, err := db.InsertTx(sql, messageTxSql)
if err != nil { return err }

aErr := mq.Publish("B-Service-topic", m)
if aErr != nil { // push failed
    messageTx.Confirm() // set status to confirm
} else {
    messageTx.Cancel() // delete the message
}

Consumers listen for confirm or try messages and retry until successful, ensuring eventual consistency without blocking the original request.

Independent Message Service and MQ Transaction

Separating the message table into an independent message service provides the same reliability guarantees but cannot be wrapped in the same local transaction, so a “prepare” state may appear if the message is created but the subsequent business operation fails.

err := request.POST("Message-Service", body)
if err != nil { return err }

aErr := request.POST("B-Service", body)
if aErr != nil { return aErr }

Some MQ implementations (e.g., RocketMQ) support transactional messages that follow the same Try‑Confirm‑Cancel pattern, adding a prepare state that the consumer must confirm before committing.

Summary

Ensuring data consistency in distributed systems inevitably requires extra mechanisms. TCC offers a flexible, service‑level solution but demands three APIs per service and careful failure handling. Local message tables are simple and work well with service calls and MQs, while MQ‑based transactions and independent message services provide a decoupled approach at the cost of added latency and limited broker support.

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.

Distributed SystemsMicroservicesData Consistencytcc
Architect's Tech Stack
Written by

Architect's Tech Stack

Java backend, microservices, distributed systems, containerized programming, and more.

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.