NashTech Blog

Table of Contents

In a Choreography-Based Saga, each service operates autonomously and communicates through events. Unlike an Orchestration-Based Saga, there is no central orchestrator. Instead, services listen for specific events, perform their respective operations, and then emit subsequent events to propagate the workflow. As a result, this decentralized approach is well-suited for simpler workflows since it eliminates bottlenecks that would otherwise be caused by a central orchestrator.

Choreophraphy-Based Saga
Choreography-Based Saga

Scenario: E-commerce Order Management System

We will use the same example as before, where a customer places an order, and multiple services handle the transaction:

  1. Order Service: Creates an order and emits an OrderCreated event.
  2. Payment Service: Listens to the OrderCreated event processes the payment and emits a PaymentProcessed or PaymentFailed event.
  3. Inventory Service: Listens to PaymentProcessed, reserves stock, and emits a StockReserved or StockUnavailable event.
  4. Notification Service: Listens to StockReserved to send a confirmation email.

Implementation of Choreography-Based Saga

Prerequisites

We need to have docker and run RabbitMQ as a docker container

docker pull rabbitmq
docker run -d --hostname rabbitmq --name rabbitmq -p 5672:5672 rabbitmq

Step 1: Setup Messaging System

Use a message broker like RabbitMQ for event-driven communication. Install the amqplib library for RabbitMQ:

npm install amqplib

Step 2: Order Service

The Order Service creates an order and emits the OrderCreated event.

// file order_service.js
const amqp = require('amqplib');
 
async function orderService() {
  const connection = await amqp.connect('amqp://localhost:5672');
  const channel = await connection.createChannel();
  await channel.assertExchange('ecommerce', 'fanout');
 
  // Simulate creating an order
  console.log('Creating order...');
  const order = { orderId: 1, customer: 'John Doe', amount: 100 };
  console.log('Order created:', order);
 
  // Emit the OrderCreated event
  channel.publish('ecommerce', '', Buffer.from(JSON.stringify({ event: 'OrderCreated', data: order })));
}
 
orderService();

Step 3: Payment Service

The Payment Service listens to the OrderCreated event processes the payment and emits either PaymentProcessed or PaymentFailed.

// file payment_service.js
const amqp = require('amqplib');
 
async function paymentService() {
  const connection = await amqp.connect('amqp://localhost:5672');
  const channel = await connection.createChannel();
  await channel.assertExchange('ecommerce', 'fanout');
  const queue = await channel.assertQueue('', { exclusive: true });
  channel.bindQueue(queue.queue, 'ecommerce', '');
 
  channel.consume(queue.queue, (msg) => {
    const { event, data } = JSON.parse(msg.content.toString());
    if (event === 'OrderCreated') {
      console.log(`Processing payment for order ${data.orderId}...`);
      // Simulate payment processing
      const paymentSuccess = true;
 
      if (paymentSuccess) {
        console.log('Payment processed.');
        channel.publish('ecommerce', '', Buffer.from(JSON.stringify({ event: 'PaymentProcessed', data })));
      } else {
        console.log('Payment failed.');
        channel.publish('ecommerce', '', Buffer.from(JSON.stringify({ event: 'PaymentFailed', data })));
      }
    }
  });
}
 
paymentService();

Step 4: Inventory Service

The Inventory Service listens to PaymentProcessed, reserves stock, and emits either StockReserved or StockUnavailable.

// file inventory_service.js
const amqp = require('amqplib');
 
async function inventoryService() {
  const connection = await amqp.connect('amqp://localhost:5672');
  const channel = await connection.createChannel();
  await channel.assertExchange('ecommerce', 'fanout');
  const queue = await channel.assertQueue('', { exclusive: true });
  channel.bindQueue(queue.queue, 'ecommerce', '');
 
  channel.consume(queue.queue, (msg) => {
    const { event, data } = JSON.parse(msg.content.toString());
    if (event === 'PaymentProcessed') {
      console.log(`Reserving stock for order ${data.orderId}...`);
      // Simulate stock reservation
      const stockAvailable = true;
 
      if (stockAvailable) {
        console.log('Stock reserved.');
        channel.publish('ecommerce', '', Buffer.from(JSON.stringify({ event: 'StockReserved', data })));
      } else {
        console.log('Stock unavailable.');
        channel.publish('ecommerce', '', Buffer.from(JSON.stringify({ event: 'StockUnavailable', data })));
      }
    }
  });
}
 
inventoryService();

Step 5: Notification Service

The Notification Service listens to StockReserved and sends a confirmation email.

// file notification_service.js
const amqp = require('amqplib');
 
async function notificationService() {
  const connection = await amqp.connect('amqp://localhost:5672');
  const channel = await connection.createChannel();
  await channel.assertExchange('ecommerce', 'fanout');
  const queue = await channel.assertQueue('', { exclusive: true });
  channel.bindQueue(queue.queue, 'ecommerce', '');
 
  channel.consume(queue.queue, (msg) => {
    const { event, data } = JSON.parse(msg.content.toString());
    if (event === 'StockReserved') {
      console.log(`Sending confirmation email for order ${data.orderId}...`);
      console.log('Email sent.');
    }
  });
}
 
notificationService();

Step 6: Start Services

node inventory_service.js
node payment_service.js
node notification_service.js
node order_service.js

Source code: https://github.com/nashtech-garage/nodejs_demo-choreography-based-saga

Workflow of Choreography-Based Saga Demo

  1. Order Service emits OrderCreated:
    • Example: { event: "OrderCreated", data: { orderId: 1, customer: "John Doe", amount: 100 } }
  2. Payment Service listens to OrderCreated:
    • Emits PaymentProcessed if successful.
    • Example: { event: "PaymentProcessed", data: { orderId: 1 } }
  3. Inventory Service listens to PaymentProcessed:
    • Emits StockReserved if stock is available.
    • Example: { event: "StockReserved", data: { orderId: 1 } }
  4. Notification Service listens to StockReserved:
    • Sends confirmation email.

Key Advantages of Choreography-Based Saga

  1. Decentralization:
    • Each service manages its logic and transitions independently, avoiding a single point of failure.
  2. Scalability:
    • The event-driven approach allows services to scale independently.
  3. Loose Coupling:
    • Services interact through events, reducing tight dependencies.

Key Considerations

  1. Event Management:
    • With many services involved, managing and tracing events can quickly become complex. To mitigate this complexity, tools like AWS CloudWatch or Jaeger can enable efficient monitoring and visualization of event flows.
  2. Error Handling:
    • Ensure compensating actions are implemented for failure scenarios (e.g., refund payment if stock is unavailable).
  3. Idempotency:
    • Services must handle duplicate events gracefully to ensure consistency and prevent data anomalies. Therefore, implementing idempotency and proper event deduplication mechanisms is crucial.

Conclusion

The Choreography-Based Saga is a decentralized approach for managing distributed transactions in microservices. It allows services to operate autonomously while maintaining consistency through event-driven communication. In Node.js, this pattern can be efficiently implemented using tools like RabbitMQ for event messaging. While it simplifies workflows, proper monitoring and error handling are essential to ensure system reliability.

References

Picture of Trần Minh

Trần Minh

I'm a solution architect at NashTech. I live and work with the quote, "Nothing is impossible; Just how to do that!". When facing problems, we can solve them by building them all from scratch or finding existing solutions and making them one. Technically, we don't have right or wrong in the choice. Instead, we choose which solutions or approaches based on input factors. Solving problems and finding reasonable solutions to reach business requirements is my favorite.

Leave a Comment

Your email address will not be published. Required fields are marked *

Suggested Article

Scroll to Top