SAGA pattern for distributed transactions

SAGA pattern for distributed transactions

Saga pattern as a method for handling distributed transactions in microservices. It contrasts the Saga approach with the naive and two-phase commit (2PC) methods, highlighting the limitations of the latter in microservices environments. The Saga pattern is described as a sequence of local transactions, each with a compensating action to ensure data consistency in case of failure

Two implementation strategies for Sagas: are choreography, where services independently react to events, and orchestration, where a central coordinator directs the transaction flow. The Saga pattern is recommended for maintaining data consistency across services while preserving the benefits of microservices architecture, such as autonomy and loose coupling.

The Saga pattern is favored over the two-phase commit approach in microservices architectures due to its asynchronous nature and better handling of long-lived transactions.

the Saga pattern is more suitable for microservices than the traditional 2PC because it avoids the synchronous and blocking nature of 2PC, which can lead to performance issues.

Implementing Sagas requires a thoughtful approach to business processes and failure scenarios, as well as the design of compensating actions to ensure effectiveness.

Saga pattern's ability to break down complex transactions into simpler, local transactions with their own rollback mechanisms, thus enhancing system reliability


The Distributed Transaction Dilemma

  • In a monolithic application, handling a customer order is striaghtforward. We typically use a single database transaction to update inventory, process payment, and create a shipping order.
  • In a microservices based architecture, each of these steps might be handled by a different service with its own database, so things are not as straightforward.

Let’s compare three approaches to handling this scenario:

  1. The Naive Approach (No Saga)
  2. The Two-Phase Commit Approach(2PC)
  3. The Saga Pattern Approach

1. The Naive Approach (No Saga)

We coordinate all these steps without any special pattern:

public void processOrder(Order order) {
    inventoryService.reduceStock(order.getItems());
    paymentService.processPayment(order.getTotal());
    shippingService.createShippingOrder(order);
}        

This looks simple, but what happens if the payment fails after we’ve already reduced the stock? Or if the shipping service is down after we’ve taken the customer’s money? We’d end up with inconsistent data across our services.

2. The Two-Phase Commit Approach (2PC)

Two-Phase Commit (2PC) is a classic approach to handling distributed transactions. It works like this:

  • Prepare Phase: The coordinator asks all participants if they’re ready to commit.
  • Commit Phase: If all participants agree, the coordinator tells everyone to commit.

public void processOrder(Order order) {
    // Prepare phase
    boolean inventoryPrepared = inventoryService.prepareReduceStock(order.getItems());
    boolean paymentPrepared = paymentService.prepareProcessPayment(order.getTotal());
    boolean shippingPrepared = shippingService.prepareCreateShippingOrder(order);

    // Commit or rollback phase
    if (inventoryPrepared && paymentPrepared && shippingPrepared) {
        // Commit
        inventoryService.reduceStock(order.getItems());
        paymentService.processPayment(order.getTotal());
        shippingService.createShippingOrder(order);
    } else {
        // Rollback (implement rollback logic for each service)
        inventoryService.rollbackReduceStock();
        paymentService.rollbackProcessPayment();
        shippingService.rollbackCreateShippingOrder();
    }
}        

While 2PC ensures consistency, it has some significant drawbacks in the microservices world:

  • It’s synchronous and blocking, which can lead to performance issues.
  • It requires all participants to support the 2PC protocol.
  • It doesn’t handle long-lived transactions well.

3. The Saga Pattern Approach

The Saga pattern takes a different approach. Instead of one big transaction, we break it down into a series of local transactions, each with its own compensating action in case of failure.

Here’s how our order process might look as a Saga:

  1. Create Order (Compensating action: Cancel Order)
  2. Reserve Inventory (Compensating action: Release Inventory)
  3. Process Payment (Compensating action: Refund Payment)
  4. Create Shipping Order (Compensating action: Cancel Shipping)

If any step fails, we trigger the compensating actions for all completed steps in reverse order.

public class OrderSaga {
    private final OrderService orderService;
    private final InventoryService inventoryService;
    private final PaymentService paymentService;
    private final ShippingService shippingService;

    public void processOrder(Order order) {
        try {
            orderService.createOrder(order);
            inventoryService.reserveInventory(order.getItems());
            paymentService.processPayment(order.getTotal());
            shippingService.createShippingOrder(order);
        } catch (Exception e) {
            compensate(order);
            throw new OrderProcessingException("Order processing failed", e);
        }
    }

    private void compensate(Order order) {
        try {
            shippingService.cancelShipping(order);
            paymentService.refundPayment(order.getTotal());
            inventoryService.releaseInventory(order.getItems());
            orderService.cancelOrder(order);
        } catch (Exception e) {
            // Log the compensation failure for manual intervention
            logger.error("Compensation failed for order: " + order.getId(), e);
        }
    }
}        

Saga pattern vs 2-Phase Commit (2PC)


Implementing Sagas: Choreography vs. Orchestration

There are two main ways to implement the Saga pattern:

  • Choreography: Each service publishes events that trigger the next service in the saga. Services independently coordinate and interact with each other based on predefined rules or events.
  • Orchestration: A central coordinator manages the entire saga flow. A central service or controller manages and directs the interactions between different services.

Choreography Example

In this approach, each service reacts to events from other services.

// OrderService.java
public class OrderService {
    private EventBus eventBus;

    public void createOrder(Order order) {
        // Create order logic
        eventBus.publish(new OrderCreatedEvent(order.getId()));
    }

    @EventHandler
    public void onPaymentProcessed(PaymentProcessedEvent event) {
        // Update order status
        eventBus.publish(new OrderCompletedEvent(event.getOrderId()));
    }
}

// InventoryService.java
public class InventoryService {
    private EventBus eventBus;

    @EventHandler
    public void onOrderCreated(OrderCreatedEvent event) {
        // Reserve inventory
        eventBus.publish(new InventoryReservedEvent(event.getOrderId()));
    }
}

// PaymentService.java
public class PaymentService {
    private EventBus eventBus;

    @EventHandler
    public void onInventoryReserved(InventoryReservedEvent event) {
        // Process payment
        eventBus.publish(new PaymentProcessedEvent(event.getOrderId()));
    }
}        

Orchestration Example

Here, a central orchestrator OrderSaga manages the entire process.

// OrderSaga.java
public class OrderSaga {
    private OrderService orderService;
    private InventoryService inventoryService;
    private PaymentService paymentService;

    public void process(String orderId) {
        try {
            Order order = orderService.getOrder(orderId);
            inventoryService.reserveInventory(order);
            paymentService.processPayment(order);
            orderService.completeOrder(orderId);
        } catch (Exception e) {
            compensate(orderId);
        }
    }

    private void compensate(String orderId) {
        // Implement compensation logic
    }
}

// OrderService.java
public class OrderService {
    public void createOrder(Order order) {
        // Create order logic
        new OrderSaga().process(order.getId());
    }
}

// InventoryService.java
public class InventoryService {
    public void reserveInventory(Order order) {
        // Reserve inventory logic
    }
}

// PaymentService.java
public class PaymentService {
    public void processPayment(Order order) {
        // Process payment logic
    }
}        


The Saga pattern is a powerful tool in the microservices toolbox. It excels in scenarios where you need to maintain data consistency across multiple services without sacrificing the autonomy and loose coupling that make microservices great.

However, implementing Sagas requires careful thought about the business process, potential failure scenarios, and how to design effective compensating actions.


要查看或添加评论,请登录

?? Saral Saxena ???????的更多文章

社区洞察

其他会员也浏览了