In this blog post we will deep dive into understanding the sage orchestration pattern and why it is needed.
Then in the subsequent blog posts, we will understand temporal and how to write temporal workflows in golang.
What is the Saga Orchestration Pattern?
The Saga Orchestration pattern is a distributed transaction pattern used in microservice architectures to ensure that a set of related transactions either all succeed or all fail together.
If you are looking to understand Microservices pattern with an example in golang, refer to one of my older blog post on the same.
It is designed to solve the problem of maintaining consistency across multiple services in a distributed system, where each service is responsible for its own data.
In the Saga Orchestration pattern, a saga is a long-lived transaction that spans multiple services and is composed of a sequence of local transactions that are executed atomically.
The local transactions are typically idempotent and can be executed independently of each other. The saga coordinates the execution of these local transactions and provides compensation logic in case of failures.
The orchestration of the saga is usually done by a dedicated service called the saga orchestrator, which is responsible for coordinating the execution of the local transactions and handling any compensating actions in case of failures.
How Does the Saga Orchestration Pattern Work?
The Saga Orchestration pattern works by breaking down a complex distributed transaction into a series of smaller, atomic transactions that can be executed independently of each other.
Each of these smaller transactions is performed by a different microservice, and the results of each transaction are communicated to the saga orchestrator.
The saga orchestrator is responsible for coordinating the execution of these local transactions and maintaining the overall consistency of the saga.
It does this by monitoring the status of each local transaction and making sure that all transactions are either successfully completed or rolled back in case of failures.
If a local transaction fails, the saga orchestrator initiates a compensating action to undo the effects of the transaction and restore the system to its previous state.
The compensating action is typically the reverse of the original transaction and is executed by the same microservice that performed the original transaction.
Benefits of the Saga Orchestration Pattern
The Saga Orchestration pattern offers several benefits over other approaches to distributed transactions:
- Increased Availability: The Saga Orchestration pattern allows services to be highly available because it does not require them to communicate with each other during the execution of the local transactions. This reduces the risk of cascading failures that can occur when one service goes down and other services become unavailable as a result.
- Improved Scalability: The Saga Orchestration pattern allows services to scale independently of each other because it does not require them to communicate with each other during the execution of the local transactions. This allows each service to scale according to its own needs and improves the overall scalability of the system.
- Flexibility: The Saga Orchestration pattern allows the system to evolve over time because it does not impose strict requirements on the services. This allows services to be added, removed, or replaced without affecting the overall consistency of the system.
- Resilience: The Saga Orchestration pattern provides resilience in the face of failures because it allows for compensating actions to be executed in case of failures. This ensures that the system can recover from failures and continue to function properly.
Drawbacks of the Saga Orchestration Pattern
While the Saga Orchestration pattern offers several benefits, there are also some drawbacks to consider:
- Increased Complexity: The Saga Orchestration pattern can be complex to implement because it requires a dedicated saga orchestrator to coordinate the execution of the local transactions and handle any compensating actions. This can add additional complexity to the system.
- Additional Overhead: The Saga Orchestration pattern can add additional overhead to the system because it requires additional communication between services to coordinate the execution of the local transactions. This can increase the latency of the system and reduce its performance.
- Limited Consistency: The Saga Orchestration pattern provides limited consistency.
Enough Theory, Show me code !
package main
import (
"fmt"
"errors"
)
// Define a type to represent a local transaction
type LocalTransaction func() error
// Define a type to represent a compensating action
type CompensatingAction func() error
// Define a type to represent a saga step
type SagaStep struct {
Transaction LocalTransaction
Compensate CompensatingAction
}
// Define a type to represent a saga
type Saga struct {
Steps []SagaStep
}
// Define a function to execute a saga
func (s *Saga) Execute() error {
// Execute each local transaction in the saga
for _, step := range s.Steps {
if err := step.Transaction(); err != nil {
// If a local transaction fails, execute the compensating actions for all previous steps
for i := len(s.Steps) - 1; i >= 0; i-- {
if err := s.Steps[i].Compensate(); err != nil {
return errors.New(fmt.Sprintf("failed to compensate for step %d: %v", i, err))
}
}
return err
}
}
return nil
}
// Define a function to perform a local transaction
func transferFunds() error {
// Perform the transfer of funds
return nil
}
// Define a function to perform a compensating action
func reverseTransfer() error {
// Reverse the transfer of funds
return nil
}
func main() {
// Define a saga consisting of two local transactions and their compensating actions
saga := Saga{
Steps: []SagaStep{
SagaStep{
Transaction: transferFunds,
Compensate: reverseTransfer,
},
SagaStep{
Transaction: transferFunds,
Compensate: reverseTransfer,
},
},
}
// Execute the saga
if err := saga.Execute(); err != nil {
fmt.Println("saga failed:", err)
} else {
fmt.Println("saga succeeded")
}
}
In this example, the Saga
type represents a saga, which is composed of a sequence of SagaStep
(s). Each SagaStep
contains a local transaction and its corresponding compensating action.
The Execute
method of the Saga
type executes each local transaction in sequence.
If a local transaction fails, it executes the compensating actions for all previous steps in reverse order to undo the effects of the failed transaction.
The example includes a simple implementation of a local transaction and a compensating action, represented by the transferFunds
and reverseTransfer
functions.
In a real-world scenario, these would likely involve more complex logic to interact with external systems (Also these steps would be orchestrated by a distributed orchestrator like temporal.)
Subscribe to my Youtube channel
Subscribe to my youtube channel if you are on the lookout for more such awesome content in video format.
Subscribe to my Newsletter
If you like my content, then consider subscribing to my free newsletter, to get exclusive, educational, technical, interesting and career related content directly delivered to your inbox
Important Links
Thanks for reading the post, be sure to follow the links below for even more awesome content in the future.
twitter: https://twitter.com/dsysd_dev
youtube: https://www.youtube.com/@dsysd-dev
github: https://github.com/dsysd-dev
medium: https://medium.com/@dsysd-dev
email: dsysd.mail@gmail.com
linkedin: https://www.linkedin.com/in/dsysd-dev/
newsletter: https://dsysd.beehiiv.com/subscribe
gumroad: https://dsysd.gumroad.com/