Distributed Transactions — 2PC vs Saga
Coordinating writes across multiple services so they all succeed or all roll back — 2PC for strong consistency, Saga for availability.
When to use
- 2PC: small-scale, homogeneous DB cluster requiring strong consistency
- Saga: microservices with high availability requirements, eventual consistency acceptable
Tradeoffs
- 2PC blocks on coordinator failure (availability risk)
- Saga needs compensating transactions (complexity, no atomicity guarantee)
- Go
- Python
type SagaStep struct {
Execute func(ctx context.Context) error
Compensate func(ctx context.Context) error
}
func RunSaga(ctx context.Context, steps []SagaStep) error {
executed := make([]SagaStep, 0, len(steps))
for _, step := range steps {
if err := step.Execute(ctx); err != nil {
// compensate in reverse order
for i := len(executed) - 1; i >= 0; i-- {
_ = executed[i].Compensate(ctx)
}
return err
}
executed = append(executed, step)
}
return nil
}
from dataclasses import dataclass
from typing import Callable
@dataclass
class SagaStep:
execute: Callable[[], None]
compensate: Callable[[], None]
def run_saga(steps: list[SagaStep]) -> None:
executed: list[SagaStep] = []
for step in steps:
try:
step.execute()
executed.append(step)
except Exception:
# compensate in reverse order
for s in reversed(executed):
s.compensate()
raise
Gotcha: 2PC at microservice scale creates a blocking protocol that destroys availability. Use Saga instead — and test your compensations.