Microservicios en JavaScript: del monolito al sistema distribuido
La transición de una aplicación monolítica a una arquitectura de microservicios no es solo una refactorización técnica, sino un cambio en la filosofía de desarrollo. En el nivel avanzado de trabajo con Node.js, nos enfrentamos a la necesidad no solo de dividir el código en módulos, sino de crear un sistema distribuido estable, escalable y tolerante a fallos. En este artículo, analizaremos las decisiones arquitectónicas clave y los patrones prácticos que le ayudarán a construir microservicios listos para producción en JavaScript.
Los principales desafíos que enfrenta un desarrollador: sincronización de datos entre servicios, mantenimiento de la integridad de las transacciones, descubrimiento de servicios y comunicación eficiente. Un error en la etapa de diseño puede llevar al caos en producción. Veamos cómo evitarlo.
1. Comunicación entre servicios: RabbitMQ y gRPC
En el mundo de los microservicios, la elección del protocolo de comunicación es clave. Para Node.js, los dos enfoques más populares son: la cola de mensajes asíncrona (RabbitMQ) y el RPC síncrono de alto rendimiento (gRPC).
Cola asíncrona (RabbitMQ)
Se utiliza para una arquitectura orientada a eventos. El servicio productor no espera una respuesta, sino que publica un evento. Esto reduce el acoplamiento. Veamos un ejemplo de envío de un pedido al servicio de envío.
// Servicio "Pedidos" — productorconst 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();}
// Llamada al crear un pedidopublishOrderCreated({ id: '123', userId: '456', amount: 1500 });El servicio consumidor (envío) se suscribe a la cola:
// Servicio "Envío" — consumidorconst 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}`); // Lógica de creación de envío channel.ack(msg); } });}
consumeOrders();gRPC síncrono
Para escenarios donde se requiere una respuesta instantánea (por ejemplo, verificar el saldo de un usuario), gRPC proporciona tipado estricto y alta velocidad a través de HTTP/2. Definimos el archivo proto:
// user.protoservice UserService { rpc GetUserBalance (UserRequest) returns (UserResponse);}
message UserRequest { string user_id = 1;}
message UserResponse { double balance = 1;}Implementación del servidor en 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; // Simulación de consulta a la BD const balance = userId === '456' ? 5000 : 0; callback(null, { balance });}
const server = new grpc.Server();server.addSe