Microservices in JavaScript: Advanced Architecture and Practice

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Microservices in JavaScript: From Monolith to Distributed System



The transition from a monolithic application to a microservices architecture is not just a technical refactoring, but a change in development philosophy. At an advanced level of working with Node.js, we face the need not just to split code into modules, but to create a resilient, scalable, and fault-tolerant distributed system. In this article, we will break down the key architectural decisions and practical patterns that will help you build production-ready microservices in JavaScript.



The main challenges a developer faces: synchronizing data between services, maintaining transaction integrity, service discovery, and efficient communication. A mistake at the design stage can lead to chaos in production. Let's look at how to avoid this.



1. Communication Between Services: RabbitMQ and gRPC



In the world of microservices, choosing the communication protocol is key. For Node.js, two approaches are most popular: an asynchronous message queue (RabbitMQ) and synchronous high-performance RPC (gRPC).



Asynchronous Queue (RabbitMQ)

Used for event-driven architecture. The producer service does not wait for a response but publishes an event. This reduces coupling. Let's look at an example of sending an order to the delivery service.



// "Orders" Service — Producerconst amqp = require('amqplib');

async function publishOrderCreated(order) { const connection = await amqp.connect('amqp://rabbitmq:5672'); const channel = await connection.createChannel(); const exchange = 'order.events'; await channel.assertExchange(exchange, 'topic', { durable: true });

channel.publish(exchange, 'order.created', Buffer.from(JSON.stringify(order)), { persistent: true });

console.log(`[x] Sent order: ${order.id}`); await channel.close(); await connection.close();}

// Called when an order is createdpublishOrderCreated({ id: '123', userId: '456', amount: 1500 });


The consumer service (delivery) subscribes to the queue:



// "Delivery" Service — Consumerconst amqp = require('amqplib');

async function consumeOrders() { const connection = await amqp.connect('amqp://rabbitmq:5672'); const channel = await connection.createChannel(); const exchange = 'order.events'; await channel.assertExchange(exchange, 'topic', { durable: true });

const q = await channel.assertQueue('', { exclusive: true }); await channel.bindQueue(q.queue, exchange, 'order.created');

channel.consume(q.queue, (msg) => { if (msg.content) { const order = JSON.parse(msg.content.toString()); console.log(`[x] Received order: ${order.id}`); // Delivery creation logic channel.ack(msg); } });}

consumeOrders();


Synchronous gRPC

For scenarios requiring an instant response (e.g., checking user balance), gRPC provides strict typing and high speed via HTTP/2. We define the proto file:



// user.protoservice UserService {  rpc GetUserBalance (UserRequest) returns (UserResponse);}

message UserRequest { string user_id = 1;}

message UserResponse { double balance = 1;}


Server implementation on Node.js:



const grpc = require('@grpc/grpc-js');const protoLoader = require('@grpc/proto-loader');const PROTO_PATH = './user.proto';

const packageDefinition = protoLoader.loadSync(PROTO_PATH, { keepCase: true, longs: String, enums: String, defaults: true, oneofs: true});const userProto = grpc.loadPackageDefinition(packageDefinition).userservice;

function getBalance(call, callback) { const userId = call.request.user_id; // Simulating a database query const balance = userId === '456' ? 5000 : 0; callback(null, { balance });}

const server = new grpc.Server();server.addSe

Blogs

Book Recommendations