DEV Community

Cover image for Building Distributed Systems with Node.js.
Sk
Sk

Posted on

Building Distributed Systems with Node.js.

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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Install dependencies:

npm i bunnimq@latest bunnimq-driver
Enter fullscreen mode Exit fullscreen mode

Source:

git clone https://github.com/SfundoMhlungu/bunni-example.git
Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

Inside the .auth file:

jane:doeeee:3
john:doeees:3
Enter fullscreen mode Exit fullscreen mode

Run the broker:

node messagebroker.js
Enter fullscreen mode Exit fullscreen mode

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"
};
Enter fullscreen mode Exit fullscreen mode

Why Distributed Systems?

The request/response cycle is fast—but short-lived.

app.get("/endpoint", (req, res) => {
  // Do work  
  res.send();
});
Enter fullscreen mode Exit fullscreen mode

This works—until you hit long-running tasks (think FFMPEG audio conversion). You can’t block the request that long.

So, we delegate:

  1. Register the job
  2. Send it to the broker
  3. Return a job ID immediately
app.get("/endpoint", (req, res) => {
  const jobId = genUniqueJobId();
  sendMessageToBroker(jobId);
  res.send(jobId);  // Client polls for progress
});
Enter fullscreen mode Exit fullscreen mode

Bunny finds an available worker, delegates the job, and you track progress (e.g., with Redis):

{
"jobId": "xyz123","progress": "20%"
}
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

Run the producer:

node send.js
Enter fullscreen mode Exit fullscreen mode

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));
});
Enter fullscreen mode Exit fullscreen mode

Run the worker:

node consume.js
Enter fullscreen mode Exit fullscreen mode

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.