Orchestrator-Based Saga in Node.js

In the Orchestrator-Based Saga, a central orchestrator service manages the flow of transactions across multiple microservices. It ensures that each step is executed in sequence and handles compensations if a step fails. Below is a full implementation example of an E-commerce Order Management System in Node.js.

Orchestrator-Based Saga Diagram
Orchestrator-Based Saga Diagram

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

Prerequisites

We need to setup docker, and run RabbitMQ as docker container

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

Step 1: Messaging Setup

Use RabbitMQ as the messaging system for communication between services. 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 handles order creation and responds to the orchestrator.

// 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 processes payments and supports refunds as a compensating action.

// 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 reserves stock and 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 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.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 demo: https://github.com/VuiLenDi/nodejs_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 the coordination of tasks, 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

Leave a comment

I’m Tran Minh

Hi, I’m Trần Minh, a Solution Architect passionate about crafting innovative and efficient solutions that make technology work seamlessly for you. Whether you’re here to explore the latest in tech or just to get inspired, I hope you find something that sparks joy and curiosity. Let’s embark on this exciting journey together!

Let’s connect