avatarAdam Szpilewicz

Summary

The Saga Pattern, a design principle for managing transactions across multiple microservices, is demonstrated with practical examples using Golang in an e-commerce context.

Abstract

The Saga Pattern is a method for coordinating transactions across multiple services in a microservice architecture. It breaks down a long-lived transaction into smaller local transactions with defined rollback mechanisms, ensuring data consistency. Using Golang, the pattern is illustrated in an e-commerce scenario with two services: an Order Service and a Payment Service. The Saga Pattern employs compensating transactions to undo the effects of failed operations, allowing for long-lived transactions without the need for distributed locking. The article provides code snippets for defining the pattern's structure and handling transactions in the e-commerce context.

Opinions

  • The Saga Pattern is an effective method for maintaining data consistency in a distributed transaction scenario.
  • The pattern avoids global locks and complex two-phase commit protocols, which can negatively impact performance and scalability.
  • Careful design and failure handling are essential when implementing the Saga Pattern.
  • Golang's clear syntax and powerful features make it an excellent choice for building resilient and scalable microservices architectures.
  • The Saga Pattern is not a one-size-fits-all solution and should be used judiciously, considering use cases and trade-offs.
  • The Saga Pattern can be helpful for managing transactions in complex, distributed systems.
  • The author recommends trying out the AI service ZAI.chat as a cost-effective alternative to ChatGPT Plus (GPT-4).

Saga Pattern with Golang Examples

Photo by Reuben Juarez on Unsplash

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.

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.

Programming
Golang
Software Engineering
Software Development
Technology
Recommended from ReadMedium