Transactions (Isolation, Retries)

Transactions in Go require strict boundary discipline to avoid deadlocks, lost updates, and partial writes. This lesson covers BeginTx usage, isolation levels, retry strategies, and production-safe transaction design.

On this page

Transactions: Where Correctness Is Won or Lost

Transactions protect data consistency. In production systems, improper transaction handling causes silent corruption, deadlocks, and high-latency cascades. Using database/sql transactions correctly requires discipline and explicit boundaries.

ACID in Practice

  • Atomicity: All operations succeed or none.
  • Consistency: Database constraints preserved.
  • Isolation: Concurrent transactions do not see inconsistent state.
  • Durability: Committed data survives crashes.

Most production bugs occur in isolation and atomicity misunderstandings.

Basic Transaction Pattern

tx, err := db.BeginTx(ctx, nil)
if err != nil {
    return err
}
defer tx.Rollback()

Always defer Rollback. If Commit succeeds, Rollback becomes a no-op.

Executing Queries Inside Transaction

_, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance - $1 WHERE id = $2", amount, fromID)
if err != nil {
    return err
}

Never use db.Exec inside a transaction block. Always use tx.

Commit and Error Handling

if err := tx.Commit(); err != nil {
    return err
}

Commit can fail. Always check its error.

Real Production Failure: Lost Update

Two concurrent transactions:

SELECT balance FROM accounts WHERE id=1;
-- both see 100

UPDATE accounts SET balance = 90 WHERE id=1;

Final balance becomes incorrect due to race.

Mitigation:

  • Use SELECT ... FOR UPDATE
  • Or optimistic locking (version column)

Isolation Levels

opts := &sql.TxOptions{
    Isolation: sql.LevelSerializable,
}
tx, err := db.BeginTx(ctx, opts)

Common levels:

  • ReadCommitted (default in many DBs)
  • RepeatableRead
  • Serializable

Higher isolation increases safety but reduces concurrency.

Deadlocks

Deadlocks occur when transactions lock resources in different orders.

Example:

  • Tx1 locks row A, waits for row B
  • Tx2 locks row B, waits for row A

Database aborts one transaction.

Mitigation

  • Access rows in consistent order
  • Keep transactions short
  • Retry serialization errors

Retry Pattern

for i := 0; i < 3; i++ {
    err := doTransaction()
    if isRetryable(err) {
        continue
    }
    return err
}

Retry only on retryable errors (deadlock, serialization failure).

Transaction Scope Discipline

Do NOT:

  • Call external APIs inside transaction
  • Perform long computations inside transaction
  • Sleep inside transaction

Transactions should be short and deterministic.

Context With Transactions

ctx, cancel := context.WithTimeout(parent, 3*time.Second)
defer cancel()

tx, err := db.BeginTx(ctx, nil)

If context times out, transaction is canceled.

Idempotent Writes

In distributed systems, retries happen. Design transactions to be idempotent.

Example: insert with unique request_id constraint.

INSERT INTO payments (request_id, amount) VALUES ($1, $2)

Duplicate request_id prevents double charge.

Outbox Pattern (Brief Intro)

When publishing events after DB write:

  • Write event into outbox table in same transaction
  • Separate worker publishes events asynchronously

This prevents partial commit + message loss.

Detecting Transaction Problems

  • High lock wait time
  • Frequent deadlocks
  • Long-running transactions
  • Pool saturation

Monitor database lock metrics.

Testing Transaction Logic

go test -race -run=Integration

Simulate concurrent operations to detect lost updates.

Common Anti-Patterns

  • Ignoring commit errors
  • Mixing db and tx inside transaction
  • Long-running transactions
  • No retry logic for serialization errors
  • External network calls inside transaction

Operational Checklist

  • Rollback deferred
  • Commit errors handled
  • Transactions short-lived
  • Isolation level explicit when needed
  • Retry strategy implemented
  • Row locking order consistent
  • No external calls inside transaction

Final Perspective

Transactions are correctness boundaries. In production systems, they must be short, isolated, and deterministic. Proper transaction discipline prevents silent data corruption and ensures predictable behavior under concurrency and failure conditions.