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.

Scenario: E-commerce Order Management
Services Involved:
- Orchestrator: Manages the saga, coordinating between all services.
- Order Service: Handles order creation.
- Payment Service: Processes payments.
- Inventory Service: Reserves stock.
- 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
- Orchestrator sends a
createOrderrequest to the Order Service. - Order Service creates the order and informs the Orchestrator.
- Orchestrator sends a
processPaymentrequest to the Payment Service. - Payment Service processes the payment and informs the Orchestrator.
- Orchestrator sends a
reserveStockrequest to the Inventory Service. - Inventory Service reserves stock and informs the Orchestrator.
- Orchestrator sends a
sendNotificationrequest to the Notification Service. - 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.








Leave a comment