9 min read

Microservices Architecture: Node.js and Message Queues

The Case for Message Queues

When building a monolithic Node.js application, services communicate through simple function calls. But as your application scales into a distributed microservices architecture, synchronous HTTP communication between services becomes a massive liability.

If Service A calls Service B via HTTP, and Service B is down, Service A fails. This cascading failure is why modern systems use Message Queues for asynchronous, decoupled communication.

Introducing RabbitMQ

RabbitMQ is a robust, open-source message broker that implements the AMQP protocol. Let's look at how we can integrate it with Node.js using the `amqplib` package.

The Publisher (Order Service)

Imagine an e-commerce platform where an Order Service needs to notify the Inventory Service that an order was placed.

const amqp = require('amqplib');

async function publishOrder(orderData) {
    try {
        const connection = await amqp.connect('amqp://localhost');
        const channel = await connection.createChannel();
        const queue = 'order_created_queue';

        // Ensure the queue exists
        await channel.assertQueue(queue, { durable: true });

        // Convert data to buffer and send
        const msg = Buffer.from(JSON.stringify(orderData));
        channel.sendToQueue(queue, msg, { persistent: true });

        console.log(" [x] Sent '%s'", orderData.orderId);
        
        setTimeout(() => {
            connection.close();
        }, 500);
    } catch (error) {
        console.error("Failed to publish message", error);
    }
}

publishOrder({ orderId: '12345', item: 'Laptop', qty: 1 });

The Consumer (Inventory Service)

The Inventory service listens to the queue and processes orders as they arrive, completely independent of the Order service's uptime.

const amqp = require('amqplib');

async function consumeOrders() {
    try {
        const connection = await amqp.connect('amqp://localhost');
        const channel = await connection.createChannel();
        const queue = 'order_created_queue';

        await channel.assertQueue(queue, { durable: true });
        
        // Only process one message at a time
        channel.prefetch(1);
        
        console.log(" [*] Waiting for messages in %s. To exit press CTRL+C", queue);

        channel.consume(queue, (msg) => {
            if (msg !== null) {
                const order = JSON.parse(msg.content.toString());
                console.log(" [x] Received order:", order.orderId);
                
                // Process inventory...
                
                // Acknowledge the message was processed
                channel.ack(msg);
            }
        }, { noAck: false });
        
    } catch (error) {
        console.error("Failed to consume messages", error);
    }
}

consumeOrders();

Resilience by Design

Notice the `persistent: true` and `channel.ack(msg)` settings. If the Inventory service crashes while processing an order, RabbitMQ will not receive an acknowledgment and will automatically requeue the message. No data is lost.

By shifting to an event-driven architecture with Node.js and message queues, you build systems that can scale horizontally and gracefully handle localized service outages.