- Published on
Saga Pattern
- Authors
- Name
- tomiwa
- @sannimichaelse
The Saga Pattern
The Saga pattern is a design approach that breaks down a complex business transaction into a series of smaller, independent steps, each handled by a separate microservice.
There are two main ways you can implement a Saga:
- Orchestration-Based Saga: In an Orchestration-Based Saga, the orchestrator acts as the conductor of an orchestra, directing and coordinating the various steps involved in the saga.The orchestrator explicitly defines the Saga's workflow, specifying the sequence of steps and the dependencies between them. This centralized approach makes it easier to visualize and reason about the overall process, as the logic is encapsulated within the orchestrator.
- Choreography-Based Saga: In a Choreography-Based Saga, each service involved in the Saga communicates directly with others by publishing events. Instead of relying on a central orchestrator to coordinate the steps, the services themselves are responsible for making decisions and reacting to events. When a service performs an action or encounters a notable condition within its domain, it publishes an event to notify other services. The event contains relevant information about the action or condition, allowing other services to react accordingly. When a service receives an event, it evaluates the event based on its own business rules and determines the appropriate course of action.
The Saga pattern is a design pattern used to manage distributed transactions in microservices architectures. It provides a way to maintain data consistency across multiple services without the need for a traditional, centralized transaction management system (like two-phase commit). Instead, it uses a sequence of local transactions, each managed by the individual microservice, combined with compensating transactions to handle failures.
Key Concepts of the Saga Pattern
Saga: A sequence of distributed transactions that work together to perform a business operation. Each step in a Saga is a local transaction that updates data within a single service.
Local Transactions: Each service involved in a Saga performs its local transaction independently. These transactions are atomic and isolated within the service.
Compensating Transactions: If any local transaction within a Saga fails, the system triggers compensating transactions to undo the changes made by preceding local transactions. This ensures that the system can be rolled back to a consistent state.
Two Types of Sagas:
Choreography-Based Saga:
- Event-Driven: Each service publishes events upon completion of its local transaction. Other services listen to these events and perform their respective local transactions.
- Decentralized: There is no central coordinator; each service knows the next step based on the event received.
- Example: An order service might create an order and publish an event. The payment service listens to the event, processes the payment, and then publishes a payment completion event for the shipping service to process the shipment.
Orchestration-Based Saga:
- Orchestrator: A central orchestrator service manages the Saga by sending commands to each service involved. The orchestrator handles the execution flow and decides when to proceed or trigger compensations.
- Centralized Control: The orchestrator directs each step, ensuring that services act in the correct sequence.
- Example: An orchestrator might send commands to the order service to create an order, then to the payment service to process payment, and finally to the shipping service to ship the product. If the payment fails, the orchestrator might trigger the order service to cancel the order.
Example of a Saga Pattern
Consider an e-commerce platform where an order is placed, payment is processed, and the product is shipped:
- Order Service: Creates an order and marks it as "Pending." It then either publishes an event (Choreography) or notifies the orchestrator (Orchestration).
- Payment Service: Listens for the order creation event (Choreography) or receives a command from the orchestrator (Orchestration) and processes the payment.
- Shipping Service: Once the payment is successful, it ships the product.
If the payment service fails, a compensating transaction is triggered to cancel the order.
Advantages of the Saga Pattern
- Scalability: Each service can independently handle its transactions, which scales well in distributed systems.
- Resilience: By using compensating transactions, the system can recover from failures and maintain consistency.
- Flexibility: Sagas can be adapted to different scenarios and are not tied to a specific database or transaction model.
Challenges of the Saga Pattern
- Complexity: Implementing and managing Sagas can be complex, especially when dealing with failures and compensations.
- Eventual Consistency: The system might not be strongly consistent at every moment, as different parts of the Saga may be at different stages.
- Error Handling: Compensating transactions might not always be simple or possible, depending on the business logic.
The Saga pattern is a powerful tool for managing distributed transactions in microservices but requires careful design and consideration of the specific business requirements.
Here's a basic example of implementing the Saga pattern in TypeScript using the Orchestration-based approach. This example simulates an order processing system involving three services: OrderService
, PaymentService
, and ShippingService
. An OrderSaga
orchestrates the Saga.
1. Define the Services
Each service will have methods for handling transactions and compensating transactions.
class OrderService {
createOrder(orderId: string) {
console.log(`Order ${orderId} created.`);
return true; // Assume the order creation is successful
}
cancelOrder(orderId: string) {
console.log(`Order ${orderId} canceled.`);
}
}
class PaymentService {
processPayment(orderId: string) {
console.log(`Payment for Order ${orderId} processed.`);
return true; // Assume the payment is successful
}
refundPayment(orderId: string) {
console.log(`Payment for Order ${orderId} refunded.`);
}
}
class ShippingService {
shipOrder(orderId: string) {
console.log(`Order ${orderId} shipped.`);
return true; // Assume the shipment is successful
}
cancelShipment(orderId: string) {
console.log(`Shipment for Order ${orderId} canceled.`);
}
}
2. Define the Orchestrator
The OrderSaga
orchestrates the Saga. It coordinates the transactions across the services and triggers compensations if something fails.
class OrderSaga {
private orderService = new OrderService();
private paymentService = new PaymentService();
private shippingService = new ShippingService();
async execute(orderId: string) {
try {
// Step 1: Create Order
const orderCreated = this.orderService.createOrder(orderId);
if (!orderCreated) throw new Error("Order creation failed");
// Step 2: Process Payment
const paymentProcessed = this.paymentService.processPayment(orderId);
if (!paymentProcessed) throw new Error("Payment processing failed");
// Step 3: Ship Order
const orderShipped = this.shippingService.shipOrder(orderId);
if (!orderShipped) throw new Error("Order shipment failed");
console.log(`Order ${orderId} completed successfully.`);
} catch (error) {
console.error(`Saga failed: ${error.message}`);
await this.compensate(orderId);
}
}
private async compensate(orderId: string) {
console.log(`Starting compensation for Order ${orderId}...`);
// Step 1: Cancel Shipment
this.shippingService.cancelShipment(orderId);
// Step 2: Refund Payment
this.paymentService.refundPayment(orderId);
// Step 3: Cancel Order
this.orderService.cancelOrder(orderId);
console.log(`Compensation for Order ${orderId} completed.`);
}
}
3. Execute the Saga
Finally, you can execute the Saga with an order ID:
(async () => {
const saga = new OrderSaga();
await saga.execute("ORDER123");
})();
Explanation
- OrderSaga: The orchestrator that handles the flow of transactions. It tries to create an order, process payment, and ship the order in sequence.
- Compensate Method: If any step fails, this method is triggered to undo the completed transactions. For example, if the payment fails, the system will attempt to cancel the order and refund the payment if needed.
Customization and Error Handling
- Error Handling: This example assumes that every transaction can succeed or fail, but you can customize error handling as needed.
- Async Operations: In a real-world scenario, operations might be asynchronous (e.g., calling external APIs). You can replace synchronous operations with
await
andPromise
handling. - Logging and Monitoring: More sophisticated implementations would include logging, monitoring, and retries.
This example provides a basic structure to implement the Saga pattern in TypeScript. In real-world applications, you would likely need more robust error handling, retries, and possibly integration with messaging systems to handle distributed transactions across different microservices.