Post 7 of 15 | Phase 3: Communication
Transporters — Making Services Talk Across Different Machines
In every post so far, all your services have been running inside one Node.js process, on one machine. The broker routes calls between them directly in memory. This is great for development but not how real microservices work in production.
In production, each service runs as a separate process, possibly on a completely different machine or container. How does the order service on Machine A call the user service on Machine B? That is exactly what Transporters solve.
What is a Transporter?
A transporter is a communication channel between brokers running on different nodes. When you add a transporter, every broker connects to it. They use it to send messages to each other.
Think of it like a walkie-talkie network. Without a transporter, each person can only talk to people in the same room. With a transporter, everyone connects to the same radio frequency and can talk to anyone regardless of where they are.
Without Transporter:
[Node A: user-service] ←→ [Node A: order-service]
Only works because they are in the same process.
With Transporter (NATS):
[Node A: user-service] ←→ [NATS Server] ←→ [Node B: order-service]
Works across machines, containers, data centers.
The best part: from your code's perspective, nothing changes. You still write ctx.call("user.getById", { id: "1" }). Moleculer figures out where the user service is and routes the call through the transporter automatically.
Available Transporters
Moleculer supports several transporters out of the box:
- TCP — built-in, no external server needed, good for simple setups
- NATS — lightweight, extremely fast, recommended for most projects
- Redis — you probably already have Redis, easy to set up
- MQTT — good for IoT applications
- AMQP — RabbitMQ, good for enterprise setups
- Kafka — good for high throughput event streaming
In this post we will cover TCP, NATS, and Redis. These three cover 90 percent of real-world use cases.
Transporter 1: TCP — No External Server Needed
The TCP transporter is built into Moleculer. You do not need to install or run any external server. Brokers discover each other automatically using UDP broadcasting on the local network.
This is perfect for:
- Local development with multiple processes
- Simple production setups where all nodes are on the same network
- Getting started without setting up NATS or Redis
// moleculer.config.js
module.exports = {
nodeID: "node-1",
transporter: "TCP"
};
That is it. Just set transporter to the string "TCP". No other configuration needed for basic usage.
Seeing TCP Transporter in Action — Two Process Setup
Let us actually run two separate Node.js processes and watch them communicate. This is the most important exercise in this post.
First, create a clean folder for this exercise:
mkdir transporter-demo
cd transporter-demo
npm init -y
npm install moleculer
Create two service files.
user-node.js — This process runs the user service
"use strict";
const { ServiceBroker } = require("moleculer");
const broker = new ServiceBroker({
nodeID: "node-user",
transporter: "TCP",
logLevel: "info"
});
broker.createService({
name: "user",
actions: {
getById(ctx) {
this.logger.info(`getById called for id: ${ctx.params.id}`);
// Simulate a database lookup
const users = {
"1": { id: "1", name: "Rahul Sharma", email: "rahul@example.com" },
"2": { id: "2", name: "Priya Singh", email: "priya@example.com" }
};
return users[ctx.params.id] || null;
}
}
});
broker.start()
.then(() => {
console.log("User node is running. Waiting for calls...");
});
order-node.js — This process runs the order service and calls the user service
"use strict";
const { ServiceBroker } = require("moleculer");
const broker = new ServiceBroker({
nodeID: "node-order",
transporter: "TCP",
logLevel: "info"
});
broker.createService({
name: "order",
actions: {
async create(ctx) {
this.logger.info(`Creating order for user: ${ctx.params.userId}`);
// This call goes to the user service on a DIFFERENT process
// Moleculer routes it automatically through TCP
const user = await ctx.call("user.getById", {
id: ctx.params.userId
});
if (!user) {
throw new Error("User not found");
}
return {
orderId: String(Date.now()),
product: ctx.params.product,
user: user.name,
status: "created"
};
}
}
});
broker.start()
.then(async () => {
console.log("Order node is running. Waiting 2 seconds for discovery...");
// Wait for service discovery to complete
await new Promise(resolve => setTimeout(resolve, 2000));
console.log("Calling order.create...");
// Call our own action which will internally call user service
const result = await broker.call("order.create", {
userId: "1",
product: "Mechanical Keyboard"
});
console.log("Result:", result);
});
Open two terminal windows. In terminal 1:
node user-node.js
In terminal 2:
node order-node.js
Watch what happens. In terminal 2 you will see:
Order node is running. Waiting 2 seconds for discovery...
Calling order.create...
Result: { orderId: '1715234567890', product: 'Mechanical Keyboard', user: 'Rahul Sharma', status: 'created' }
In terminal 1 you will see:
getById called for id: 1
The order process called the user service running in a completely separate process. The code inside order-node.js looks exactly the same as if user service was in the same process. Moleculer handled everything.
Transporter 2: NATS
NATS is a lightweight, extremely fast messaging server. It is the recommended transporter for most Moleculer production setups. It is faster than Redis for messaging and simpler than Kafka.
Install NATS Server
On Windows, download the NATS server executable from nats.io/download. Extract it and run:
nats-server
You should see:
[1] Starting nats-server
[1] Listening for client connections on 0.0.0.0:4222
[1] Server is ready
Install the NATS client package in your project:
npm install nats
Configure the transporter:
// moleculer.config.js
module.exports = {
nodeID: "node-1",
transporter: "nats://localhost:4222"
};
Or with full options:
module.exports = {
nodeID: "node-1",
transporter: {
type: "NATS",
options: {
url: "nats://localhost:4222",
// If NATS requires authentication
user: "admin",
pass: "password"
}
}
};
Everything else in your code stays exactly the same. Just changing the transporter string is enough.
Transporter 3: Redis
If you already have Redis running for caching, you can use it as a transporter too. Install the Redis client:
npm install ioredis
Configure:
// moleculer.config.js
module.exports = {
nodeID: "node-1",
transporter: "redis://localhost:6379"
};
Or with options:
module.exports = {
nodeID: "node-1",
transporter: {
type: "Redis",
options: {
host: "localhost",
port: 6379,
password: "your-redis-password",
db: 0
}
}
};
Redis transporter is convenient if you already have Redis in your infrastructure. For dedicated messaging, NATS is faster.
Service Discovery — How Brokers Find Each Other
When a new broker starts and connects to the transporter, it announces itself to all other brokers. This is called service discovery. Every broker maintains a registry of all known services across all nodes.
Node A starts → announces "I have user-service" to NATS
Node B starts → announces "I have order-service" to NATS
Node A hears → "Oh, Node B has order-service. I will remember that."
Node B hears → "Oh, Node A has user-service. I will remember that."
Now:
Node B calls ctx.call("user.getById") →
Moleculer checks registry → user-service is on Node A →
Routes call through NATS to Node A →
Node A executes the action →
Result comes back to Node B
This happens automatically. You never write any of this yourself.
You can see the registry in action using the REPL:
npm run repl
Type:
nodes
This shows all connected nodes.
services
This shows all registered services across all nodes.
Namespace with Transporter
When you use a transporter, the namespace option in moleculer.config.js becomes very important. It isolates your application from other Moleculer apps using the same transporter server.
// App 1 — e-commerce
module.exports = {
namespace: "ecommerce",
transporter: "nats://localhost:4222"
};
// App 2 — blog platform (using same NATS server)
module.exports = {
namespace: "blog",
transporter: "nats://localhost:4222"
};
These two apps share the same NATS server but never interfere with each other. Their messages are isolated by namespace. Always set a namespace in production.
Multiple Instances of the Same Service
One of the biggest benefits of using a transporter is horizontal scaling. You can run multiple instances of the same service on different nodes. Moleculer automatically load balances requests across them.
Node A: user-service (instance 1)
Node B: user-service (instance 2)
Node C: user-service (instance 3)
When order-service calls "user.getById":
→ Request 1 goes to Node A
→ Request 2 goes to Node B
→ Request 3 goes to Node C
→ Request 4 goes to Node A (round robin repeats)
No configuration needed. The moment multiple instances of the same service exist, Moleculer distributes the load automatically using the strategy defined in moleculer.config.js. Default is RoundRobin.
What Happens When a Node Goes Down
When a node disconnects, the transporter notifies all other brokers. They remove that node's services from their registry. Calls to those services are routed to other available instances. If no instances are available, Moleculer throws a ServiceNotFoundError.
Node A: user-service ← goes down
Node B: user-service ← still running
order-service calls "user.getById"
→ Moleculer sees Node A is gone
→ Routes to Node B automatically
→ No error, no downtime
This is the foundation of fault tolerance in Moleculer. We build on top of this with Circuit Breakers and Retries in Post 8.
Heartbeats
Brokers send heartbeat signals to each other through the transporter at regular intervals. The default is every 10 seconds. If a broker misses heartbeats for 30 seconds, it is considered dead and removed from the registry.
You can configure this in moleculer.config.js:
module.exports = {
heartbeatInterval: 10, // Send heartbeat every 10 seconds
heartbeatTimeout: 30 // Consider node dead after 30 seconds of silence
};
In production you might want to reduce these values for faster failure detection. But lower values mean more network traffic.
Using Transporter in Your My-Project
To enable transporter in the project you already have, open moleculer.config.js and change:
// Before
transporter: null,
// After (TCP — no extra setup needed)
transporter: "TCP",
Or if you have NATS running:
transporter: "nats://localhost:4222",
Then run your project normally with npm run dev. Everything works exactly the same. The only difference is that if you open another terminal and start another Node.js process with the same transporter and a different service, they will automatically discover each other and communicate.
Development Tip — When to Use a Transporter
During the early stages of development, transporter: null is fine. All services in one process is simpler and faster to work with. Use a transporter when:
- You are ready to split services into separate processes
- You want to test horizontal scaling
- You are preparing for production deployment
For this course, keep transporter: null until Post 14 where we set up Docker and run each service as a separate container. At that point we will switch to Redis transporter because Redis will already be in our Docker setup.
Summary
- A transporter connects brokers running on different machines or processes.
- Without a transporter, all services must be in the same Node.js process.
- With a transporter, services can run anywhere and still call each other transparently.
- TCP transporter is built-in and needs no external server. Good for local multi-process setups.
- NATS is the recommended production transporter. Lightweight and extremely fast.
- Redis transporter is convenient if you already have Redis in your infrastructure.
- Service discovery is automatic. Brokers announce themselves when they connect.
- Multiple instances of the same service are load balanced automatically using RoundRobin by default.
- When a node goes down, other nodes detect it via missed heartbeats and stop routing to it.
- Namespace isolates multiple Moleculer apps using the same transporter server.
- Your action code never changes regardless of whether you use a transporter or not.
Up Next
Post 8 covers Fault Tolerance — Circuit Breaker, Retry, Timeout, and Bulkhead. These are the mechanisms that keep your application running even when individual services fail or slow down. This is what separates a production-ready microservices system from a fragile one.
Course Progress: 7 of 15 posts complete.
No comments:
Post a Comment