Saga Pattern
Sequence of local transactions with compensation. No distributed locks, better scalability.
Problem: How to maintain consistency across multiple services?
Traditional solution: 2PC (Two-Phase Commit) - but it’s:
Solution: Saga Pattern - Sequence of local transactions with compensation!
The Saga pattern is a distributed transaction management pattern that breaks a long-running transaction into a sequence of smaller, local transactions. Each local transaction commits independently, and if any step fails, the saga executes compensating transactions to undo the effects of previously completed steps.
Unlike traditional ACID transactions that use two-phase commit (2PC) to ensure atomicity across services, sagas use eventual consistency. Each step in a saga is a local ACID transaction within a single service. If a step fails, instead of rolling back (which isn’t possible across services), the saga executes compensating actions in reverse order.
Key Insight: You can’t “rollback” a committed transaction in another service (the flight is already booked), but you can compensate (cancel the flight). Compensation is not the same as rollback - it’s a new business operation that undoes the effect of a previous operation.
Central coordinator orchestrates all steps.
Characteristics:
from enum import Enumfrom typing import List, Callable, Dict, Anyfrom dataclasses import dataclass
class SagaStatus(Enum): PENDING = "pending" IN_PROGRESS = "in_progress" COMPENSATING = "compensating" COMPLETED = "completed" FAILED = "failed"
@dataclassclass SagaStep: """Saga step definition""" name: str execute: Callable # Step execution function compensate: Callable # Compensation function service: str
class SagaOrchestrator: """Saga orchestrator"""
def __init__(self): self.steps: List[SagaStep] = [] self.executed_steps: List[SagaStep] = [] self.status = SagaStatus.PENDING
def add_step(self, step: SagaStep): """Add step to saga""" self.steps.append(step)
def execute(self, context: Dict[str, Any]) -> bool: """Execute saga""" self.status = SagaStatus.IN_PROGRESS
try: for step in self.steps: print(f"Executing step: {step.name}")
# Execute step result = step.execute(context)
if not result: # Step failed - compensate print(f"Step {step.name} failed. Compensating...") self._compensate() return False
# Track executed step self.executed_steps.append(step) context[f"{step.name}_result"] = result
# All steps succeeded self.status = SagaStatus.COMPLETED return True
except Exception as e: print(f"Saga failed: {e}") self._compensate() return False
def _compensate(self): """Compensate executed steps (in reverse order)""" self.status = SagaStatus.COMPENSATING
# Compensate in reverse order for step in reversed(self.executed_steps): try: print(f"Compensating step: {step.name}") step.compensate(step) except Exception as e: print(f"Compensation failed for {step.name}: {e}") # Log but continue compensating
self.status = SagaStatus.FAILED
# Example: Create Order Sagadef create_order(context): print("Creating order...") # Call order service return {'order_id': 123}
def cancel_order(step): print("Cancelling order...") # Call order service to cancel
def reserve_inventory(context): print("Reserving inventory...") # Call inventory service return {'reservation_id': 456}
def release_inventory(step): print("Releasing inventory...") # Call inventory service to release
def charge_payment(context): print("Charging payment...") # Call payment service return False # Simulate failure
def refund_payment(step): print("Refunding payment...") # Call payment service to refund
# Create sagaorchestrator = SagaOrchestrator()orchestrator.add_step(SagaStep( name='create_order', execute=create_order, compensate=cancel_order, service='order-service'))orchestrator.add_step(SagaStep( name='reserve_inventory', execute=reserve_inventory, compensate=release_inventory, service='inventory-service'))orchestrator.add_step(SagaStep( name='charge_payment', execute=charge_payment, compensate=refund_payment, service='payment-service'))
# Execute sagasuccess = orchestrator.execute({})import java.util.*;
enum SagaStatus { PENDING, IN_PROGRESS, COMPENSATING, COMPLETED, FAILED}
class SagaStep { private final String name; private final Function<Map<String, Object>, Object> execute; private final Consumer<SagaStep> compensate; private final String service;
// Constructor, getters...}
class SagaOrchestrator { private final List<SagaStep> steps = new ArrayList<>(); private final List<SagaStep> executedSteps = new ArrayList<>(); private SagaStatus status = SagaStatus.PENDING;
public void addStep(SagaStep step) { steps.add(step); }
public boolean execute(Map<String, Object> context) { status = SagaStatus.IN_PROGRESS;
try { for (SagaStep step : steps) { System.out.println("Executing step: " + step.getName());
// Execute step Object result = step.getExecute().apply(context);
if (result == null) { // Step failed - compensate System.out.println("Step " + step.getName() + " failed. Compensating..."); compensate(); return false; }
// Track executed step executedSteps.add(step); context.put(step.getName() + "_result", result); }
// All steps succeeded status = SagaStatus.COMPLETED; return true; } catch (Exception e) { System.err.println("Saga failed: " + e.getMessage()); compensate(); return false; } }
private void compensate() { status = SagaStatus.COMPENSATING;
// Compensate in reverse order Collections.reverse(executedSteps); for (SagaStep step : executedSteps) { try { System.out.println("Compensating step: " + step.getName()); step.getCompensate().accept(step); } catch (Exception e) { System.err.println("Compensation failed for " + step.getName() + ": " + e.getMessage()); } }
status = SagaStatus.FAILED; }}enum SagaStatus { PENDING = "pending", IN_PROGRESS = "in_progress", COMPENSATING = "compensating", COMPLETED = "completed", FAILED = "failed"}
interface SagaStep { name: string; execute: (context: Record<string, any>) => Promise<any>; compensate: (step: SagaStep) => Promise<void>; service: string;}
class SagaOrchestrator { private steps: SagaStep[] = []; private executedSteps: SagaStep[] = []; private status: SagaStatus = SagaStatus.PENDING;
addStep(step: SagaStep): void { // Add step to saga this.steps.push(step); }
async execute(context: Record<string, any>): Promise<boolean> { // Execute saga this.status = SagaStatus.IN_PROGRESS;
try { for (const step of this.steps) { console.log(`Executing step: ${step.name}`);
// Execute step const result = await step.execute(context);
if (!result) { // Step failed - compensate console.log(`Step ${step.name} failed. Compensating...`); await this.compensate(); return false; }
// Track executed step this.executedSteps.push(step); context[`${step.name}_result`] = result; }
// All steps succeeded this.status = SagaStatus.COMPLETED; return true; } catch (error) { console.error(`Saga failed: ${error}`); await this.compensate(); return false; } }
private async compensate(): Promise<void> { // Compensate executed steps (in reverse order) this.status = SagaStatus.COMPENSATING;
// Compensate in reverse order for (const step of this.executedSteps.reverse()) { try { console.log(`Compensating step: ${step.name}`); await step.compensate(step); } catch (error) { console.error(`Compensation failed for ${step.name}: ${error}`); // Log but continue compensating } }
this.status = SagaStatus.FAILED; }}
// Example: Create Order Sagaasync function createOrder(context: Record<string, any>): Promise<any> { console.log("Creating order..."); // Call order service return { order_id: 123 };}
async function cancelOrder(step: SagaStep): Promise<void> { console.log("Cancelling order..."); // Call order service to cancel}
async function reserveInventory(context: Record<string, any>): Promise<any> { console.log("Reserving inventory..."); // Call inventory service return { reservation_id: 456 };}
async function releaseInventory(step: SagaStep): Promise<void> { console.log("Releasing inventory..."); // Call inventory service to release}
async function chargePayment(context: Record<string, any>): Promise<any> { console.log("Charging payment..."); // Call payment service throw new Error("Payment failed"); // Simulate failure}
async function refundPayment(step: SagaStep): Promise<void> { console.log("Refunding payment..."); // Call payment service to refund}
// Create sagaconst orchestrator = new SagaOrchestrator();orchestrator.addStep({ name: 'create_order', execute: createOrder, compensate: cancelOrder, service: 'order-service'});orchestrator.addStep({ name: 'reserve_inventory', execute: reserveInventory, compensate: releaseInventory, service: 'inventory-service'});orchestrator.addStep({ name: 'charge_payment', execute: chargePayment, compensate: refundPayment, service: 'payment-service'});
// Execute sagaconst success = await orchestrator.execute({});#include <string>#include <vector>#include <functional>#include <map>#include <iostream>
enum class SagaStatus { PENDING, IN_PROGRESS, COMPENSATING, COMPLETED, FAILED};
struct SagaStep { std::string name; std::function<std::map<std::string, int>(std::map<std::string, int>&)> execute; std::function<void(const SagaStep&)> compensate; std::string service;};
class SagaOrchestrator {private: std::vector<SagaStep> steps; std::vector<SagaStep> executedSteps; SagaStatus status = SagaStatus::PENDING;
public: void addStep(const SagaStep& step) { // Add step to saga steps.push_back(step); }
bool execute(std::map<std::string, int>& context) { // Execute saga status = SagaStatus::IN_PROGRESS;
try { for (const auto& step : steps) { std::cout << "Executing step: " << step.name << std::endl;
// Execute step auto result = step.execute(context);
if (result.empty()) { // Step failed - compensate std::cout << "Step " << step.name << " failed. Compensating..." << std::endl; compensate(); return false; }
// Track executed step executedSteps.push_back(step); context[step.name + "_result"] = result.begin()->second; }
// All steps succeeded status = SagaStatus::COMPLETED; return true; } catch (const std::exception& e) { std::cerr << "Saga failed: " << e.what() << std::endl; compensate(); return false; } }
private: void compensate() { // Compensate executed steps (in reverse order) status = SagaStatus::COMPENSATING;
// Compensate in reverse order for (auto it = executedSteps.rbegin(); it != executedSteps.rend(); ++it) { try { std::cout << "Compensating step: " << it->name << std::endl; it->compensate(*it); } catch (const std::exception& e) { std::cerr << "Compensation failed for " << it->name << ": " << e.what() << std::endl; // Log but continue compensating } }
status = SagaStatus::FAILED; }};
// Example usagestd::map<std::string, int> createOrder(std::map<std::string, int>& context) { std::cout << "Creating order..." << std::endl; return {{"order_id", 123}};}
void cancelOrder(const SagaStep& step) { std::cout << "Cancelling order..." << std::endl;}
int main() { SagaOrchestrator orchestrator;
orchestrator.addStep({ "create_order", createOrder, cancelOrder, "order-service" });
std::map<std::string, int> context; bool success = orchestrator.execute(context);
return success ? 0 : 1;}using System;using System.Collections.Generic;using System.Threading.Tasks;
public enum SagaStatus { Pending, InProgress, Compensating, Completed, Failed}
public class SagaStep { public string Name { get; set; } public Func<Dictionary<string, object>, Task<object>> Execute { get; set; } public Func<SagaStep, Task> Compensate { get; set; } public string Service { get; set; }}
public class SagaOrchestrator { private readonly List<SagaStep> steps = new List<SagaStep>(); private readonly List<SagaStep> executedSteps = new List<SagaStep>(); private SagaStatus status = SagaStatus.Pending;
public void AddStep(SagaStep step) { // Add step to saga steps.Add(step); }
public async Task<bool> ExecuteAsync(Dictionary<string, object> context) { // Execute saga status = SagaStatus.InProgress;
try { foreach (var step in steps) { Console.WriteLine($"Executing step: {step.Name}");
// Execute step var result = await step.Execute(context);
if (result == null) { // Step failed - compensate Console.WriteLine($"Step {step.Name} failed. Compensating..."); await CompensateAsync(); return false; }
// Track executed step executedSteps.Add(step); context[$"{step.Name}_result"] = result; }
// All steps succeeded status = SagaStatus.Completed; return true; } catch (Exception e) { Console.Error.WriteLine($"Saga failed: {e.Message}"); await CompensateAsync(); return false; } }
private async Task CompensateAsync() { // Compensate executed steps (in reverse order) status = SagaStatus.Compensating;
// Compensate in reverse order executedSteps.Reverse(); foreach (var step in executedSteps) { try { Console.WriteLine($"Compensating step: {step.Name}"); await step.Compensate(step); } catch (Exception e) { Console.Error.WriteLine($"Compensation failed for {step.Name}: {e.Message}"); // Log but continue compensating } }
status = SagaStatus.Failed; }}
// Example: Create Order Sagaasync Task<object> CreateOrder(Dictionary<string, object> context) { Console.WriteLine("Creating order..."); // Call order service return new { order_id = 123 };}
async Task CancelOrder(SagaStep step) { Console.WriteLine("Cancelling order..."); // Call order service to cancel}
// Usagevar orchestrator = new SagaOrchestrator();orchestrator.AddStep(new SagaStep { Name = "create_order", Execute = CreateOrder, Compensate = CancelOrder, Service = "order-service"});
var context = new Dictionary<string, object>();bool success = await orchestrator.ExecuteAsync(context);Distributed coordination. Each service knows next step.
Characteristics:
class OrderService: """Order service - starts saga"""
def create_order(self, order_data): """Create order and publish event""" # Create order (local transaction) order = self._create_order_local(order_data)
# Publish event event_bus.publish('OrderCreated', { 'order_id': order.id, 'user_id': order.user_id, 'amount': order.amount })
return order
def handle_payment_failed(self, event): """Compensate: Cancel order""" order_id = event['order_id'] self._cancel_order(order_id)
class InventoryService: """Inventory service - listens to OrderCreated"""
def handle_order_created(self, event): """Reserve inventory""" try: # Reserve inventory (local transaction) reservation = self._reserve_inventory_local( event['order_id'], event['items'] )
# Publish success event event_bus.publish('InventoryReserved', { 'order_id': event['order_id'], 'reservation_id': reservation.id }) except Exception as e: # Publish failure event event_bus.publish('InventoryReservationFailed', { 'order_id': event['order_id'], 'error': str(e) })
def handle_payment_failed(self, event): """Compensate: Release inventory""" order_id = event['order_id'] self._release_inventory(order_id)
class PaymentService: """Payment service - listens to InventoryReserved"""
def handle_inventory_reserved(self, event): """Charge payment""" try: # Charge payment (local transaction) payment = self._charge_payment_local( event['order_id'], event['amount'] )
# Publish success event event_bus.publish('PaymentCharged', { 'order_id': event['order_id'], 'payment_id': payment.id }) except Exception as e: # Publish failure event - triggers compensation event_bus.publish('PaymentFailed', { 'order_id': event['order_id'], 'error': str(e) })
# Setup event subscriptionsevent_bus.subscribe('OrderCreated', inventory_service.handle_order_created)event_bus.subscribe('InventoryReserved', payment_service.handle_inventory_reserved)event_bus.subscribe('PaymentFailed', order_service.handle_payment_failed)event_bus.subscribe('PaymentFailed', inventory_service.handle_payment_failed)class OrderService { public void createOrder(OrderData orderData) { // Create order (local transaction) Order order = createOrderLocal(orderData);
// Publish event eventBus.publish("OrderCreated", Map.of( "order_id", order.getId(), "user_id", order.getUserId(), "amount", order.getAmount() )); }
@EventHandler public void handlePaymentFailed(PaymentFailedEvent event) { // Compensate: Cancel order cancelOrder(event.getOrderId()); }}
class InventoryService { @EventHandler public void handleOrderCreated(OrderCreatedEvent event) { try { // Reserve inventory (local transaction) Reservation reservation = reserveInventoryLocal( event.getOrderId(), event.getItems() );
// Publish success event eventBus.publish("InventoryReserved", Map.of( "order_id", event.getOrderId(), "reservation_id", reservation.getId() )); } catch (Exception e) { // Publish failure event eventBus.publish("InventoryReservationFailed", Map.of( "order_id", event.getOrderId(), "error", e.getMessage() )); } }
@EventHandler public void handlePaymentFailed(PaymentFailedEvent event) { // Compensate: Release inventory releaseInventory(event.getOrderId()); }}class OrderService { // Order service - starts saga constructor(private eventBus: EventBus) {}
createOrder(orderData: OrderData): Order { // Create order and publish event // Create order (local transaction) const order = this.createOrderLocal(orderData);
// Publish event this.eventBus.publish('OrderCreated', { order_id: order.id, user_id: order.userId, amount: order.amount });
return order; }
handlePaymentFailed(event: PaymentFailedEvent): void { // Compensate: Cancel order this.cancelOrder(event.orderId); }
private createOrderLocal(orderData: OrderData): Order { // Local transaction implementation return { id: 123, userId: orderData.userId, amount: orderData.amount }; }
private cancelOrder(orderId: number): void { // Cancel order implementation console.log(`Cancelling order: ${orderId}`); }}
class InventoryService { // Inventory service - listens to OrderCreated constructor(private eventBus: EventBus) {}
handleOrderCreated(event: OrderCreatedEvent): void { // Reserve inventory try { // Reserve inventory (local transaction) const reservation = this.reserveInventoryLocal( event.orderId, event.items );
// Publish success event this.eventBus.publish('InventoryReserved', { order_id: event.orderId, reservation_id: reservation.id }); } catch (error) { // Publish failure event this.eventBus.publish('InventoryReservationFailed', { order_id: event.orderId, error: error.message }); } }
handlePaymentFailed(event: PaymentFailedEvent): void { // Compensate: Release inventory this.releaseInventory(event.orderId); }
private reserveInventoryLocal(orderId: number, items: Item[]): Reservation { // Local transaction implementation return { id: 456 }; }
private releaseInventory(orderId: number): void { // Release inventory implementation console.log(`Releasing inventory for order: ${orderId}`); }}
class PaymentService { // Payment service - listens to InventoryReserved constructor(private eventBus: EventBus) {}
handleInventoryReserved(event: InventoryReservedEvent): void { // Charge payment try { // Charge payment (local transaction) const payment = this.chargePaymentLocal( event.orderId, event.amount );
// Publish success event this.eventBus.publish('PaymentCharged', { order_id: event.orderId, payment_id: payment.id }); } catch (error) { // Publish failure event - triggers compensation this.eventBus.publish('PaymentFailed', { order_id: event.orderId, error: error.message }); } }
private chargePaymentLocal(orderId: number, amount: number): Payment { // Local transaction implementation throw new Error("Payment failed"); // Simulate failure }}
// Setup event subscriptionsconst eventBus = new EventBus();const orderService = new OrderService(eventBus);const inventoryService = new InventoryService(eventBus);const paymentService = new PaymentService(eventBus);
eventBus.subscribe('OrderCreated', (event) => { inventoryService.handleOrderCreated(event);});eventBus.subscribe('InventoryReserved', (event) => { paymentService.handleInventoryReserved(event);});eventBus.subscribe('PaymentFailed', (event) => { orderService.handlePaymentFailed(event); inventoryService.handlePaymentFailed(event);});#include <string>#include <map>#include <functional>#include <iostream>
class EventBus {public: void publish(const std::string& eventType, const std::map<std::string, int>& data) { // Publish event implementation std::cout << "Publishing event: " << eventType << std::endl; }
void subscribe(const std::string& eventType, std::function<void(const std::map<std::string, int>&)> handler) { // Subscribe to event implementation }};
class OrderService { // Order service - starts sagaprivate: EventBus& eventBus;
public: OrderService(EventBus& eventBus) : eventBus(eventBus) {}
void createOrder(const std::map<std::string, int>& orderData) { // Create order and publish event // Create order (local transaction) int orderId = createOrderLocal(orderData);
// Publish event std::map<std::string, int> eventData; eventData["order_id"] = orderId; eventBus.publish("OrderCreated", eventData); }
void handlePaymentFailed(const std::map<std::string, int>& event) { // Compensate: Cancel order cancelOrder(event.at("order_id")); }
private: int createOrderLocal(const std::map<std::string, int>& orderData) { // Local transaction implementation return 123; }
void cancelOrder(int orderId) { // Cancel order implementation std::cout << "Cancelling order: " << orderId << std::endl; }};
class InventoryService { // Inventory service - listens to OrderCreatedprivate: EventBus& eventBus;
public: InventoryService(EventBus& eventBus) : eventBus(eventBus) {}
void handleOrderCreated(const std::map<std::string, int>& event) { // Reserve inventory try { // Reserve inventory (local transaction) int reservationId = reserveInventoryLocal(event.at("order_id"));
// Publish success event std::map<std::string, int> eventData; eventData["order_id"] = event.at("order_id"); eventData["reservation_id"] = reservationId; eventBus.publish("InventoryReserved", eventData); } catch (const std::exception& e) { // Publish failure event std::map<std::string, int> eventData; eventData["order_id"] = event.at("order_id"); eventBus.publish("InventoryReservationFailed", eventData); } }
void handlePaymentFailed(const std::map<std::string, int>& event) { // Compensate: Release inventory releaseInventory(event.at("order_id")); }
private: int reserveInventoryLocal(int orderId) { // Local transaction implementation return 456; }
void releaseInventory(int orderId) { // Release inventory implementation std::cout << "Releasing inventory for order: " << orderId << std::endl; }};
// Usageint main() { EventBus eventBus; OrderService orderService(eventBus); InventoryService inventoryService(eventBus);
// Setup subscriptions // eventBus.subscribe("OrderCreated", [&inventoryService](const auto& event) { // inventoryService.handleOrderCreated(event); // });
return 0;}using System;using System.Collections.Generic;
public class EventBus { private readonly Dictionary<string, List<Action<Dictionary<string, object>>>> handlers = new();
public void Publish(string eventType, Dictionary<string, object> data) { // Publish event implementation if (handlers.ContainsKey(eventType)) { foreach (var handler in handlers[eventType]) { handler(data); } } }
public void Subscribe(string eventType, Action<Dictionary<string, object>> handler) { // Subscribe to event implementation if (!handlers.ContainsKey(eventType)) { handlers[eventType] = new List<Action<Dictionary<string, object>>>(); } handlers[eventType].Add(handler); }}
public class OrderService { // Order service - starts saga private readonly EventBus eventBus;
public OrderService(EventBus eventBus) { this.eventBus = eventBus; }
public void CreateOrder(Dictionary<string, object> orderData) { // Create order and publish event // Create order (local transaction) var order = CreateOrderLocal(orderData);
// Publish event eventBus.Publish("OrderCreated", new Dictionary<string, object> { ["order_id"] = order["id"], ["user_id"] = order["user_id"], ["amount"] = order["amount"] }); }
public void HandlePaymentFailed(Dictionary<string, object> eventData) { // Compensate: Cancel order CancelOrder((int)eventData["order_id"]); }
private Dictionary<string, object> CreateOrderLocal(Dictionary<string, object> orderData) { // Local transaction implementation return new Dictionary<string, object> { ["id"] = 123, ["user_id"] = orderData["user_id"], ["amount"] = orderData["amount"] }; }
private void CancelOrder(int orderId) { // Cancel order implementation Console.WriteLine($"Cancelling order: {orderId}"); }}
public class InventoryService { // Inventory service - listens to OrderCreated private readonly EventBus eventBus;
public InventoryService(EventBus eventBus) { this.eventBus = eventBus; }
public void HandleOrderCreated(Dictionary<string, object> eventData) { // Reserve inventory try { // Reserve inventory (local transaction) var reservation = ReserveInventoryLocal((int)eventData["order_id"]);
// Publish success event eventBus.Publish("InventoryReserved", new Dictionary<string, object> { ["order_id"] = eventData["order_id"], ["reservation_id"] = reservation["id"] }); } catch (Exception e) { // Publish failure event eventBus.Publish("InventoryReservationFailed", new Dictionary<string, object> { ["order_id"] = eventData["order_id"], ["error"] = e.Message }); } }
public void HandlePaymentFailed(Dictionary<string, object> eventData) { // Compensate: Release inventory ReleaseInventory((int)eventData["order_id"]); }
private Dictionary<string, object> ReserveInventoryLocal(int orderId) { // Local transaction implementation return new Dictionary<string, object> { ["id"] = 456 }; }
private void ReleaseInventory(int orderId) { // Release inventory implementation Console.WriteLine($"Releasing inventory for order: {orderId}"); }}
// Usagevar eventBus = new EventBus();var orderService = new OrderService(eventBus);var inventoryService = new InventoryService(eventBus);
// Setup event subscriptionseventBus.Subscribe("OrderCreated", inventoryService.HandleOrderCreated);eventBus.Subscribe("PaymentFailed", orderService.HandlePaymentFailed);eventBus.Subscribe("PaymentFailed", inventoryService.HandlePaymentFailed);| Aspect | Orchestrator | Choreography |
|---|---|---|
| Control | Centralized | Distributed |
| Complexity | Lower | Higher |
| Coupling | Higher | Lower |
| Debugging | Easier | Harder |
| Scalability | Lower | Higher |
| Use Case | Complex workflows | Simple workflows |
Choose Orchestrator when:
Choose Choreography when:
Undo the action (if possible):
Opposite transaction:
Automatic expiration:
Saga Pattern
Sequence of local transactions with compensation. No distributed locks, better scalability.
Orchestrator
Central coordinator. Easier to understand and debug. Good for complex workflows.
Choreography
Distributed coordination. More decoupled and scalable. Good for simple workflows.
Compensation
Undo completed steps when saga fails. Not rollback, but compensating action. Critical for consistency.