In a Choreography-Based Saga, each service is autonomous and communicates through events. There is no central orchestrator. Services listen for specific events, perform their operations, and emit subsequent events to continue the workflow. This decentralized approach works well for simpler workflows and reduces bottlenecks caused by a central orchestrator.

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:
- Order Service: Creates an order and emits an
OrderCreatedevent. - Payment Service: Listens to the
OrderCreatedevent, processes the payment, and emits aPaymentProcessedorPaymentFailedevent. - Inventory Service: Listens to
PaymentProcessed, reserves stock, and emits aStockReservedorStockUnavailableevent. - Notification Service: Listens to
StockReservedto send a confirmation email.
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: 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 demo: https://github.com/VuiLenDi/nodejs_choreography-based-saga
Workflow
- Order Service emits
OrderCreated:- Example:
{ event: "OrderCreated", data: { orderId: 1, customer: "John Doe", amount: 100 } }
- Example:
- Payment Service listens to
OrderCreated:- Emits
PaymentProcessedif successful. - Example:
{ event: "PaymentProcessed", data: { orderId: 1 } }
- Emits
- Inventory Service listens to
PaymentProcessed:- Emits
StockReservedif stock is available. - Example:
{ event: "StockReserved", data: { orderId: 1 } }
- Emits
- Notification Service listens to
StockReserved:- Sends confirmation email.
Key Advantages of Choreography-Based Saga
- Decentralization:
- Each service manages its own logic and transitions independently, avoiding a single point of failure.
- Scalability:
- The event-driven approach allows services to scale independently.
- Loose Coupling:
- Services interact through events, reducing tight dependencies.
Key Considerations
- Event Management:
- With many services, managing and tracing events can become complex. Use tools like AWS CloudWatch or Jaeger for monitoring.
- Error Handling:
- Ensure compensating actions are implemented for failure scenarios (e.g., refund payment if stock is unavailable).
- Idempotency:
- Services must handle duplicate events gracefully to avoid inconsistent states.
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.








Leave a comment