In microservices architectures adhering to the Database-Per-Service pattern, traditional ACID transactions coordinated across services via Two-Phase Commit (2PC) or XA protocols impose unacceptable latency, lock contention, and availability degradation. The Saga Pattern has emerged as the industry standard for ensuring eventual consistency across distributed domains.
1. The Saga Paradigm
A Saga is a sequence of local transactions. Each local transaction updates the database within a single service and publishes a message or event to trigger the next transaction in a downstream service. If a local transaction fails, the Saga executes a series of Compensating Transactions that undo the changes made by the preceding local transactions.
2. Orchestration vs Choreography Comparison
| Attribute | Choreography (Event-Driven) | Orchestration (Central Coordinator) |
|---|---|---|
| Coordination | Services react to events independently | A central service commands participants |
| Maintainability | Difficult to trace as step count increases | Centralized state machine; straightforward debugging |
| Coupling | Loose coupling via event brokers | Participants depend on orchestrator interface |
| Best For | Simple workflows (2-3 services) | Complex multi-step business logic (Fintech, Orders) |
3. The Transactional Outbox Pattern
How do we atomically update the database and emit an event to Apache Kafka without dual-write failures? The solution is committing the event into a dedicated outbox table inside the identical database transaction:
func CreateOrderWithOutbox(ctx context.Context, db *sql.DB, order *Order) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
// 1. Mutate business entity
_, err = tx.ExecContext(ctx, "INSERT INTO orders (id, user_id, amount, status) VALUES ($1, $2, $3, $4)",
order.ID, order.UserID, order.Amount, "PENDING")
if err != nil {
return err
}
// 2. Persist event into Outbox within the identical atomic transaction
payload, _ := json.Marshal(order)
_, err = tx.ExecContext(ctx, "INSERT INTO outbox_events (aggregate_type, aggregate_id, type, payload) VALUES ($1, $2, $3, $4)",
"ORDER", order.ID, "OrderCreated", payload)
if err != nil {
return err
}
return tx.Commit()
}