Golang / GoLang System Architecture and Testing Interview Questions
How do you implement the Saga pattern for distributed transactions in Go?
Distributed transactions that span multiple services cannot use traditional 2-phase commit without creating tight coupling and availability issues. The Saga pattern decomposes a transaction into a sequence of local transactions, each publishing an event. Failures trigger compensating transactions.
// Choreography-based saga: services react to events // OrderService publishes OrderCreated type OrderCreatedEvent struct { OrderID string `json:"order_id"` UserID string `json:"user_id"` Amount float64 `json:"amount"` ProductID string `json:"product_id"` } // PaymentService consumes OrderCreated, publishes PaymentProcessed or PaymentFailed func (s *PaymentService) HandleOrderCreated(ctx context.Context, event OrderCreatedEvent) error { charged, err := s.stripe.Charge(ctx, event.UserID, event.Amount) if err != nil { // Publish compensating event for OrderService to cancel the order return s.publisher.Publish(ctx, PaymentFailedEvent{ OrderID: event.OrderID, Reason: err.Error(), }) } return s.publisher.Publish(ctx, PaymentProcessedEvent{ OrderID: event.OrderID, ChargeID: charged.ID, }) } // InventoryService consumes PaymentProcessed, publishes StockReserved or StockUnavailable // OrderService listens to StockReserved â fulfillment // OrderService listens to StockUnavailable â refund (another compensating event) // Outbox pattern: ensure event is published atomically with DB write func (s *OrderService) CreateOrder(ctx context.Context, req OrderRequest) error { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() // Write order to DB orderID := uuid.New().String() tx.ExecContext(ctx, "INSERT INTO orders ...", orderID, req.UserID) // Write event to outbox table in SAME transaction eventData, _ := json.Marshal(OrderCreatedEvent{OrderID: orderID}) tx.ExecContext(ctx, "INSERT INTO outbox (event_type, payload) VALUES ($1, $2)", "order.created", eventData) return tx.Commit() // Separate process reads outbox and publishes to message queue }
Outbox pattern solves the dual-write problem: writing to the DB and publishing to a message queue are two separate operations — either can fail. Writing both to the same DB transaction (with the event in an outbox table) makes them atomic; a relay process then publishes to the queue and deletes from outbox.
More Related questions...