Marcio Cunha

Distributed Transaction Processing with Two-Phase Commit and SAGA Patterns in Go Microservices

Learn how to ensure data consistency in microservices using Go. We analyze the limits of Two-Phase Commit and the asynchronous resilience of SAGA patterns.

Marcio Cunha•5 min
Also available in:EspañolPortuguês
Summary
  • Distributed systems require breaking down monoliths, creating complex challenges for data synchronization across isolated databases.
  • The Two-Phase Commit protocol offers strict consistency but drastically sacrifices availability and locks resources during failures.
  • The SAGA pattern replaces synchronous locks with a chain of local transactions combined with compensating actions.
  • The Go language enables the implementation of resilient transactions by combining goroutines and channels to manage asynchronous flows.
  • Choosing between immediate or eventual consistency depends directly on the business model and system latency tolerance.

The Data Consistency Challenge in Microservices

When breaking a large monolithic system into several smaller microservices, each piece gets its own isolated database. In practice, this means a simple e-commerce purchase—which previously updated inventory, charged the credit card, and generated an invoice in a single database transaction—now needs to coordinate multiple independent services across the network. If the network fails halfway through, the system ends up with inconsistent data, such as the customer being charged but the product remaining out of stock.

Ensuring that all steps succeed or everything is rolled back is the core problem of distributed transactions. In software engineering, we strive for consistency to prevent money from disappearing or orders from getting lost in digital limbo. To solve this, the industry created approaches like the two-step protocol and sequences of compensating steps, each carrying deep performance and complexity trade-offs.

How Two-Phase Commit Works in Practice

The Two-Phase Commit, known as 2PC, is a classic protocol that attempts to ensure multiple databases update their records simultaneously. In the first phase, called preparation, a central coordinator asks all participating databases if they can save the data. Each database checks its locks, ensures there is space, and responds with a yes or no vote. In the second phase, if everyone voted yes, the coordinator issues the command to permanently commit the write.

The major pitfall of Two-Phase Commit is blocking. While databases wait for the final coordinator order, they keep records locked to prevent concurrent changes. In practice, if the coordinator crashes or the network slows down during this waiting window, resources become unavailable, degrading overall system performance. Because of this fragility to network drops, modern architectures avoid 2PC in high-scale environments.

The SAGA Pattern Alternative for High Availability

To escape the rigid locks of 2PC, modern architecture embraces the SAGA pattern. Instead of a single global transaction that locks everything, SAGA breaks the process down into a sequence of local transactions. Each microservice executes its operation in its own database and emits an event for the next step. In practice, this means consistency shifts from immediate to eventual, meaning the system converges toward the correct state asynchronously.

The standout feature of SAGA is compensation. If the third step in a five-step flow fails, the system cannot simply trigger an automatic rollback, because previous steps have already confirmed changes in separate databases. What SAGA does is trigger compensating transactions in reverse order, such as issuing a card refund or returning the item to inventory. It is like undoing an unbaked cake already placed in the oven through a controlled correction process.

Implementing Asynchronous Transactions in Go

The Go language offers excellent tools to handle asynchronous flows and concurrency through goroutines (lightweight tasks running in parallel) and communication channels. When building a SAGA in Go, we model each step of the flow as an independent command that can be executed, monitored, and reverted if necessary. Creating clean structures helps keep the code readable and easy to unit test.

package main

import (
	"context"
	"fmt"
	"time"
)

type Step struct {
	Name         string
	Execute      func(ctx context.Context) error
	Compensate   func(ctx context.Context) error
}

func RunSaga(ctx context.Context, steps []Step) error {
	var executed []Step
	for _, step := range steps {
		fmt.Printf("Executing step: %s\n", step.Name)
		if err := step.Execute(ctx); err != nil {
			fmt.Printf("Error in step %s. Starting compensations...\n", step.Name)
			reverseSteps(ctx, executed)
			return err
		}
		executed = append(executed, step)
	}
	return nil
}

func reverseSteps(ctx context.Context, steps []Step) {
	for i := len(steps) - 1; i >= 0; i-- {
		step := steps[i]
		if step.Compensate != nil {
			_ = step.Compensate(ctx)
		}
	}
}

func main() {
	ctx := context.Background()
	steps := []Step{
		{
			Name: "ReserveInventory",
			Execute: func(c context.Context) error { return nil },
			Compensate: func(c context.Context) error { fmt.Println("Inventory returned."); return nil },
		},
	}
	_ = RunSaga(ctx, steps)
}

The code above demonstrates a basic SAGA orchestration structure in Go, where a slice stores successful steps to ensure reversion occurs in the correct order if something goes wrong. This approach guarantees total control over the flow without relying on complex, blocking network protocols.

Orchestration versus Choreography in Distributed Systems

When implementing the SAGA pattern, engineers must decide between two control models: orchestration or choreography. In choreography, microservices talk to each other via events published to a message bus, without a central controller. Each service listens to what interests it, does its job, and notifies the next. In practice, this reduces structural coupling but makes it harder to visualize the complete flow as the system grows.

In orchestration, however, there is a dedicated component—called an orchestrator—whose sole responsibility is dictating step order and deciding when to trigger compensations. Although it adds a central point of dependency, the orchestrator drastically simplifies debugging and failure tracking in complex enterprise environments. Choosing between the two paths depends on team maturity and the visibility required by business rules.

Final Thoughts on Consistency in Modern Architectures

Distributed transaction processing makes it clear that software engineering is the art of managing trade-offs. While Two-Phase Commit pursues strict consistency that penalizes availability, SAGA patterns embrace the imperfect reality of distributed networks through eventual consistency and compensating actions. Understanding these differences prevents fragile architectures and ensures systems capable of scaling safely.

Mastering these tools in efficient languages like Go allows you to build resilient microservices prepared to handle inevitable infrastructure failures without corrupting user data. Careful flow planning and clear compensation rules remain the fundamental pillars for the success of any modern, large-scale application.