Around Go: MongoDB and Redis · 3 MIN READ

MongoDB transactions: retry database work, not external effects

Choose transaction boundaries, concerns and idempotency for ambiguous failures.

A transaction can protect a multi-document invariant, but it adds coordination and can retry work. A callback that charges a card or sends email while updating documents can repeat an external effect. Keep the database transition inside the transaction and move external delivery behind an explicit durable boundary.

Around Go: data stores · A MAP OF THE IDEA

  1. Business operation ID
  2. Transaction callback
  3. Commit outcome
  4. Outbox delivery

Useful when

  • Choose transaction boundaries, concerns and idempotency for ambiguous failures.

Watch out

  • The driver can rerun the transaction callback. MongoDB cannot roll back an external payment or email, so make database work repeatable and deliver external effects through an idempotent boundary.

Implementation & operation cost: Transactions add coordination and contention; the outbox adds durable records, delivery workers and duplicate handling.

01

Keep session work sequential and owned

The Go v2 driver’s WithTransaction callback receives the context that operations in the transaction must use. Do not share a session concurrently across goroutines or start parallel operations inside one transaction. A replica-set or supported sharded deployment is required; a standalone local server is not equivalent.

go
session, err := client.StartSession()
if err != nil { return err }
defer func() {
    cleanup, stop := context.WithTimeout(context.Background(), time.Second)
    defer stop()
    session.EndSession(cleanup)
}()
_, err = session.WithTransaction(ctx, func(tx context.Context) (any, error) {
    if _, err := orders.InsertOne(tx, order); err != nil { return nil, err }
    return outbox.InsertOne(tx, event)
})
return err
Read the code

The application constructs order and event with stable operation identifiers and uses collections from the same shared client. The callback contains database work only; the outbox consumer sends the external notification after commit.

02

Define what committed means for this operation

Read concern, write concern and read preference affect visibility and acknowledgment. A timeout or lost response can leave the caller unsure whether a commit happened. Reconcile using the durable operation ID rather than assuming every error means nothing changed.

javascript
db.orders.createIndex({operationID: 1}, {unique: true})
db.outbox.createIndex({eventID: 1}, {unique: true})
db.orders.findOne({operationID: "checkout-demo-42"})
Read the code

These development commands illustrate the durable keys used for reconciliation. Unique identifiers do not on their own deliver an external effect exactly once; the receiver and delivery process also need idempotency.

03

Test the failure windows

Inject a transient transaction failure, an ambiguous commit response and a consumer crash after sending but before marking delivery. Verify the resulting invariant and duplicate handling. Short transactions reduce contention; do not perform slow HTTP calls or user interaction while holding a transaction open.

TAKE THIS WITH YOU

A retryable transaction callback should contain repeatable database work. Use durable operation IDs and an outbox when the business action crosses systems.

Check your understanding

Why keep an external payment call outside WithTransaction?

Go to the source

Progress stays in this browser. No account needed.