NashTech Blog

Table of Contents

In the Orchestrator-Based Saga, a central orchestrator service systematically manages transactions across multiple microservices. Specifically, it ensures that each step is executed in the correct sequence and handles compensations whenever a step fails. Below is a complete implementation example of an E-commerce Order Management System in Node.js to illustrate this concept further.

Orchestrator-Based Saga
Orchestrator-Based Saga

Scenario: E-commerce Order Management

Services Involved:

  1. Orchestrator: Manages the saga, coordinating between all services.
  2. Order Service: Handles order creation.
  3. Payment Service: Processes payments.
  4. Inventory Service: Reserves stock.
  5. Notification Service: Sends order confirmation notifications.

Step-by-Step Implementation of Orchestrator-Based Saga

Prerequisites

We need to set up docker and run RabbitMQ as a docker container

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

Step 1: Messaging Setup

To facilitate communication between services, RabbitMQ is used as the messaging system. First, install the necessary library:

npm install amqplib

Step 2: Orchestrator Service

The Orchestrator coordinates the flow of the saga, handling each step and compensating when necessary.

// file orchestrator_service.js
const amqp = require('amqplib');
 
async function orchestratorService() {
  const connection = await amqp.connect('amqp://localhost:5672');
  const channel = await connection.createChannel();
  await channel.assertQueue('orchestrator');
 
  console.log('Orchestrator is running...');
 
  // Listen for responses from services
  channel.consume('orchestrator', async (msg) => {
    const { step, status, data } = JSON.parse(msg.content.toString());
 
    if (status === 'success') {
      if (step === 'orderCreated') {
        console.log('Order created, proceeding to payment...');
        channel.sendToQueue('payment', Buffer.from(JSON.stringify({ action: 'processPayment', data })));
      } else if (step === 'paymentProcessed') {
        console.log('Payment processed, proceeding to inventory...');
        channel.sendToQueue('inventory', Buffer.from(JSON.stringify({ action: 'reserveStock', data })));
      } else if (step === 'stockReserved') {
        console.log('Stock reserved, sending notification...');
        channel.sendToQueue('notification', Buffer.from(JSON.stringify({ action: 'sendNotification', data })));
      }
    } else {
      console.error(`Error in ${step}, compensating...`);
      if (step === 'paymentProcessed') {
        console.log('Refunding payment...');
        channel.sendToQueue('payment', Buffer.from(JSON.stringify({ action: 'refundPayment', data })));
      } else if (step === 'stockReserved') {
        console.log('Releasing stock...');
        channel.sendToQueue('inventory', Buffer.from(JSON.stringify({ action: 'releaseStock', data })));
      }
    }
 
    channel.ack(msg);
  });
 
  // Start the saga by sending the first message
  const orderData = { orderId: 1, customer: 'John Doe', amount: 100 };
  channel.sendToQueue('order', Buffer.from(JSON.stringify({ action: 'createOrder', data: orderData })));
}
 
orchestratorService();

Step 3: Order Service

The Order Service is responsible for handling order creation and subsequently responding to the orchestrator to proceed with the workflow.

// 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.assertQueue('order');
 
  console.log('Order Service is running...');
 
  channel.consume('order', (msg) => {
    const { action, data } = JSON.parse(msg.content.toString());
 
    if (action === 'createOrder') {
      console.log(`Creating order ${data.orderId}...`);
      // Simulate success
      const response = { step: 'orderCreated', status: 'success', data };
      channel.sendToQueue('orchestrator', Buffer.from(JSON.stringify(response)));
    }
 
    channel.ack(msg);
  });
}
 
orderService();

Step 4: Payment Service

The Payment Service handles payment processing and additionally supports refunds as a necessary compensating action when needed.

// 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.assertQueue('payment');
 
  console.log('Payment Service is running...');
 
  channel.consume('payment', (msg) => {
    const { action, data } = JSON.parse(msg.content.toString());
 
    if (action === 'processPayment') {
      console.log(`Processing payment for order ${data.orderId}...`);
      // Simulate success
      const response = { step: 'paymentProcessed', status: 'success', data };
      channel.sendToQueue('orchestrator', Buffer.from(JSON.stringify(response)));
    } else if (action === 'refundPayment') {
      console.log(`Refunding payment for order ${data.orderId}...`);
    }
 
    channel.ack(msg);
  });
}
 
paymentService();

Step 5: Inventory Service

The Inventory Service manages stock reservations and, if needed, supports releasing stock as a compensating action.

// 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.assertQueue('inventory');
 
  console.log('Inventory Service is running...');
 
  channel.consume('inventory', (msg) => {
    const { action, data } = JSON.parse(msg.content.toString());
 
    if (action === 'reserveStock') {
      console.log(`Reserving stock for order ${data.orderId}...`);
      // Simulate success
      const response = { step: 'stockReserved', status: 'success', data };
      channel.sendToQueue('orchestrator', Buffer.from(JSON.stringify(response)));
    } else if (action === 'releaseStock') {
      console.log(`Releasing stock for order ${data.orderId}...`);
    }
 
    channel.ack(msg);
  });
}
 
inventoryService();

Step 6: Notification Service

The Notification Service is responsible for sending a confirmation email to notify users upon successful order processing.

// 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.assertQueue('notification');
 
  console.log('Notification Service is running...');
 
  channel.consume('notification', (msg) => {
    const { action, data } = JSON.parse(msg.content.toString());
 
    if (action === 'sendNotification') {
      console.log(`Sending confirmation email for order ${data.orderId}...`);
      console.log('Notification sent successfully.');
    }
 
    channel.ack(msg);
  });
}
 
notificationService();

Step 6: Start Services

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

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

Workflow

  1. Orchestrator sends a createOrder request to the Order Service.
  2. Order Service creates the order and informs the Orchestrator.
  3. Orchestrator sends a processPayment request to the Payment Service.
  4. Payment Service processes the payment and informs the Orchestrator.
  5. Orchestrator sends a reserveStock request to the Inventory Service.
  6. Inventory Service reserves stock and informs the Orchestrator.
  7. Orchestrator sends a sendNotification request to the Notification Service.
  8. Notification Service sends a confirmation email.

If any step fails, the orchestrator triggers compensating actions (e.g., refunding payment or releasing stock).

Conclusion

The Orchestrator-Based Saga pattern provides a centralized approach to managing distributed transactions in microservices, making it ideal for complex workflows that require precise control, error handling, and monitoring. By centralizing task coordination, this pattern ensures consistency and simplifies the implementation of compensating actions in case of failures.

However, the reliance on a central orchestrator introduces potential bottlenecks and tighter coupling, requiring robust scalability and fault-tolerance mechanisms. Despite these considerations, Orchestrator-Based Saga remains a powerful solution for systems where clear workflow definition, centralized monitoring, and predictable transaction management are critical. When implemented effectively, it ensures reliability and resilience in distributed architectures.

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