Golang / GoLang System Architecture and Testing Interview Questions
How do you ensure data consistency across Go microservices without distributed transactions?
Distributed transactions (2PC) are generally avoided in microservices — they couple services and create availability issues. The alternative is eventual consistency through careful design: idempotent consumers, compensating transactions, and the Outbox pattern.
// Pattern: last-write-wins with optimistic locking (version field) type User struct { ID int Name string Email string Version int // incremented on each update } func (r *UserRepo) UpdateOptimistic( ctx context.Context, user *User, ) error { result, err := r.db.ExecContext(ctx, `UPDATE users SET name=$1, email=$2, version=version+1 WHERE id=$3 AND version=$4`, user.Name, user.Email, user.ID, user.Version, ) if err != nil { return err } n, _ := result.RowsAffected() if n == 0 { return ErrConflict // another update happened concurrently } user.Version++ // reflect new version return nil } // Pattern: idempotent event handler with deduplication type EventHandler struct { db *sql.DB dedup *DedupeCache } func (h *EventHandler) Handle(ctx context.Context, event Event) error { // Use event ID as idempotency key if already, _ := h.dedup.Check(ctx, event.ID); already { return nil // already processed â safe to ack } tx, err := h.db.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() // Process the event if err := h.applyEvent(ctx, tx, event); err != nil { return fmt.Errorf("apply event: %w", err) } // Mark event as processed within the same transaction tx.ExecContext(ctx, "INSERT INTO processed_events (id) VALUES ($1)", event.ID) if err := tx.Commit(); err != nil { return fmt.Errorf("commit: %w", err) } h.dedup.Set(ctx, event.ID) // populate cache return nil }
More Related questions...