What I Can’t Build, I Don’t Understand!
A distributed system is multiple computers acting as one to a client.
You’d be surprised how common distributed systems are! Even a small B2B startup running three servers—one in Go, another in Python, and one in Node.js—is technically distributed.
I learned this the hard way. Three months into my first dev job, I got thrown into building one. It wasn’t planned, it wasn’t in my job description—but I had no choice. Most tutorials? Useless in those moments. I had to get battle-tested. Scars and all.
Now I’m here to break it down for you.
Let’s spin up a distributed system with a pure JavaScript Broker.
Distributed Systems
I’m more practical than theoretical—I’d rather build first, debate definitions later.
Forget the academic nitpicking. Here’s a functional definition:
A distributed system is like a hive—machines working together with a single brain. To the client, it feels like one computer.
We can build that.
The simplest server setup is what I call a unit system (the "NAND gate of the backend"):
client -> server -> [database, storage bucket]
Interview Tip: In system design interviews, start simple—one user, one server. Nail that, then scale. Works every time.
Project Setup
Create the project structure:
producer\
send.js
worker\
consume.js
.auth
messagebroker.js
package.json
Install dependencies:
npm i bunnimq@latest bunnimq-driver
Source:
git clone https://github.com/SfundoMhlungu/bunni-example.git
Message Brokers: The Hive Mind
The message broker is the hive’s brain—coordinating producers and consumers.
Setting Up the Broker
In messagebroker.js:
import Bunny, { OPTS } from "bunnimq";
import path from "path";
import { fileURLToPath } from "url";
OPTS.cwd = path.dirname(fileURLToPath(import.meta.url)); // Read the .auth file
Bunny({ port: 3000, DEBUG: true });
Inside the .auth file:
jane:doeeee:3
john:doeees:3
Run the broker:
node messagebroker.js
When Bunny boots, it looks for two things:
- .auth file – like user entries in a DB.
- Cached queues – for message durability.
Permission mapping:
const perms = {
1: "PUBLISH",
2: "CONSUME",
3: "PUBLISH|CONSUME",
4: "ADMIN"
};
Why Distributed Systems?
The request/response cycle is fast—but short-lived.
app.get("/endpoint", (req, res) => {
// Do work
res.send();
});
This works—until you hit long-running tasks (think FFMPEG audio conversion). You can’t block the request that long.
So, we delegate:
- Register the job
- Send it to the broker
- Return a job ID immediately
app.get("/endpoint", (req, res) => {
const jobId = genUniqueJobId();
sendMessageToBroker(jobId);
res.send(jobId); // Client polls for progress
});
Bunny finds an available worker, delegates the job, and you track progress (e.g., with Redis):
{
"jobId": "xyz123","progress": "20%"
}
The Producer: Sending Work
In send.js:
import Bunnymq from "bunnimq-driver";
const bunny = new Bunnymq({
port: 3000,
host: "localhost",
username: "john",
password: "doeees" // Match your .auth creds
});
// Create or connect to the queue
bunny.QueueDeclare(
{
name: "myqueue",
config: {
QueueExpiry: 60,
MessageExpiry: 20,
AckExpiry: 10,
Durable: true, // Persist messages
noAck: false // Expect acknowledgment
}
},
(res) => console.log("Queue creation:", res)
);
// Publish 500 jobs
for (let i = 0; i < 500; i++) {
bunny.Publish(`${Math.random()}-${i + 800}`, (res) => console.log(res));
}
Run the producer:
node send.js
You’ll see "Trying to send" logs from Bunni—but no processing yet.
No workers exist...yet.
The Worker: Processing Jobs
In consume.js:
import Bunnymq from "bunnimq-driver";
const bunny = new Bunnymq({
port: 3000,
host: "localhost",
username: "john",
password: "doeees"
});
// Connect to the queue
bunny.QueueDeclare({ name: "myqueue", config: undefined }, (res) => {
console.log("Queue connection:", res);
});
let consumed = 0;
// Consume messages
bunny.Consume("myqueue", async (msg) => {
console.log("Processing:", msg);
consumed++;
const [id, time] = msg.split("-");
console.log(`Job ID: ${id}, Duration: ${time}ms`);
await new Promise((resolve) => setTimeout(resolve, Number(time)));
console.log(`Jobs consumed: ${consumed}`);
bunny.Ack((success) => console.log("Ready for more work:", success));
});
Run the worker:
node consume.js
Once the worker connects and the exchange handshakes, it’ll start receiving messages.
Want to see the hive in action? Open multiple terminals and launch more workers—they’ll automatically share the workload.
This is scaling. This is the hive mind.
In a real-world scenario, you’d wrap both producer and consumer inside a proper server application for better management.
This article is part of larger series where we build bunni from scratch, for developers interested networks systems and want to go beyond CRUD:
In this article, we covered:
✅ The basics of distributed systems
✅ A custom message broker in Node.js
✅ Scaling consumers to distribute work
see ya!🫡
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.