Saga Pattern with Golang Examples
If you enjoy reading medium articles and are interested in becoming a member, I would be happy to share my referral link with you!
In the realm of distributed systems and microservices, the Saga Pattern emerges as a compelling strategy for managing transactions that span multiple services. The Saga Pattern provides a way to define a series of local transactions, each of which can be undone if necessary. This mechanism facilitates long-lived transactions without the need for distributed locking.
What is the Saga Pattern?
In essence, the Saga Pattern is a design principle used to manage transactions that span across multiple microservices. It splits a long-lived transaction into multiple smaller transactions, each corresponding to a local transaction in a particular service. These transactions are coordinated in a specific sequence, ensuring data consistency across services.
The Saga Pattern comes into play when you need to maintain data consistency across multiple services in a microservice architecture, where each service has its own database. Each step in a saga represents a transaction in a particular service and has a defined rollback mechanism to ensure data consistency in case of a failure.
The Saga Pattern in Action with Golang
Now, let’s dive into a practical example using Golang. Suppose we have a simple e-commerce system consisting of two services: an Order Service and a Payment Service. A user places an order, and a corresponding payment is processed.
![](https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*GUAt1wuoYvrgGIRSd7f8kQ.png)
First, we need to define the basic structure for our services and the actions they will take.
type OrderService struct {}
func (os *OrderService) Order(item string, qty int) error {
// perform order operation
}
func (os *OrderService) CancelOrder(orderID string) error {
// perform cancel operation
}
type PaymentService struct {}
func (ps *PaymentService) Pay(orderID string, amount float64) error {
// perform payment operation
}
func (ps *PaymentService) Refund(paymentID string) error {
// perform refund operation
}
In the Saga Pattern, each transaction is paired with a compensating transaction that can undo the original transaction. For instance, if the Order
operation is successful but the Pay
operation fails, the CancelOrder
operation (compensating transaction) would be executed to ensure data consistency.
type Saga struct {
steps []SagaStep
}
type SagaStep struct {
action func() error
compensation func() error
}
func (s *Saga) AddStep(action func() error, compensation func() error) {
step := SagaStep{action, compensation}
s.steps = append(s.steps, step)
}
func (s *Saga) Execute() error {
for _, step := range s.steps {
if err := step.action(); err != nil {
return s.Compensate()
}
}
return nil
}
func (s *Saga) Compensate() error {
for i := len(s.steps) - 1; i >= 0; i-- {
if err := s.steps[i].compensation(); err != nil {
return err
}
}
return nil
}
When we execute the Saga, it tries to execute all the steps in order. If any step fails, it immediately starts the compensation process, executing all the compensating transactions in reverse order.
In our e-commerce scenario, the application of the Saga Pattern might look like this:
orderService := &OrderService{}
paymentService := &PaymentService{}
saga := &Saga{}
saga.AddStep(
func() error { return orderService.Order("item1", 2) },
func() error { return orderService.CancelOrder("order1") },
)
saga.AddStep(
func() error { return paymentService.Pay("order1", 100.0) },
func() error { return paymentService.Refund("payment1") },
)
if err := saga.Execute(); err != nil {
log.Fatalf("failed to execute saga: %v", err)
}
In the above code snippet, the saga is defined with two steps. The first step orders an item, and if it fails, it compensates by cancelling the order. The second step processes the payment, and if it fails, it compensates by refunding the payment. The saga is then executed, and if it fails, it logs the error.
The whole code snippet
package main
import (
"errors"
"fmt"
"log"
)
type OrderService struct{}
func (os *OrderService) Order(item string, qty int) error {
log.Println("Order placed successfully")
return nil // return errors.New("order failed") to simulate order failure
}
func (os *OrderService) CancelOrder(orderID string) error {
log.Println("Order cancelled successfully")
return nil
}
type PaymentService struct{}
func (ps *PaymentService) Pay(orderID string, amount float64) error {
log.Println("Payment processed successfully")
return nil // return errors.New("payment failed") to simulate payment failure
}
func (ps *PaymentService) Refund(paymentID string) error {
log.Println("Payment refunded successfully")
return nil
}
type Saga struct {
steps []SagaStep
}
type SagaStep struct {
action func() error
compensation func() error
}
func (s *Saga) AddStep(action func() error, compensation func() error) {
step := SagaStep{action, compensation}
s.steps = append(s.steps, step)
}
func (s *Saga) Execute() error {
for _, step := range s.steps {
if err := step.action(); err != nil {
log.Println("Saga failed. Starting compensation transactions...")
return s.Compensate()
}
}
return nil
}
func (s *Saga) Compensate() error {
for i := len(s.steps) - 1; i >= 0; i-- {
if err := s.steps[i].compensation(); err != nil {
return err
}
}
return nil
}
func main() {
orderService := &OrderService{}
paymentService := &PaymentService{}
saga := &Saga{}
saga.AddStep(
func() error { return orderService.Order("item1", 2) },
func() error { return orderService.CancelOrder("order1") },
)
saga.AddStep(
func() error { return paymentService.Pay("order1", 100.0) },
func() error { return paymentService.Refund("payment1") },
)
if err := saga.Execute(); err != nil {
log.Fatalf("failed to execute saga: %v", err)
}
fmt.Println("Saga completed successfully.")
}
Conclusion
The Saga Pattern is an effective method for maintaining data consistency in a distributed transaction scenario. It provides a way to coordinate multiple steps, each of which has a compensating transaction in case of failure. By using this pattern, we can avoid global locks and complex two-phase commit protocols that can hurt the performance and scalability of the system. However, the Saga Pattern requires careful design and handling of failures, so it’s crucial to thoroughly understand its implications and constraints.
With Golang, we can effectively implement the Saga Pattern due to its robust error handling mechanisms and native support for concurrency. As demonstrated in the examples, Golang’s clear syntax and powerful features make it an excellent choice for building resilient and scalable microservices architectures.
Remember, like any architectural pattern, the Saga Pattern is not a silver bullet and should not be used everywhere. It’s important to consider your use case and understand the trade-offs before adopting any specific pattern.