1. The Problem
Build a load balancer that hands out requests across a pool of servers in round-robin order, skipping any server currently marked unhealthy. If every server is down, it returns nothing.
Why this project? A load balancer sits in front of every scaled web system (Nginx, HAProxy, AWS ELB). The core — pick the next server fairly, route around failures — is a clean, real algorithm.
2. How to Think About This
Keep the server list, a health flag per server, and a pointer to the next one to try. nextServer walks forward from the pointer, advancing round-robin (wrapping with modulo). It returns the first healthy server it lands on, and gives up after a full loop if all are down. Marking a server unhealthy just flips its flag — the selection loop skips it automatically.
3. The Build
export class LoadBalancer {
constructor(servers) {
this.servers = servers; // list of server names
this.healthy = new Map(servers.map(s => [s, true]));
this.current = 0; // round-robin pointer
}
markUnhealthy(server) { this.healthy.set(server, false); }
markHealthy(server) { this.healthy.set(server, true); }
nextServer() {
// Round-robin, skipping unhealthy servers.
for (let i = 0; i < this.servers.length; i++) {
const server = this.servers[this.current];
this.current = (this.current + 1) % this.servers.length;
if (this.healthy.get(server)) return server;
}
return null; // all servers are down
}
}
// Demo
const lb = new LoadBalancer(["server1", "server2", "server3"]);
console.log(Array.from({ length: 4 }, () => lb.nextServer())); // rotates
lb.markUnhealthy("server2");
console.log(Array.from({ length: 4 }, () => lb.nextServer())); // skips server2
4. Test & Prove
import { LoadBalancer } from "./balancer.mjs";
let pass = 0, fail = 0;
function test(desc, fn) {
try { fn(); console.log(" \u2713", desc); pass++; }
catch (e) { console.log(" \u2717", desc, "\u2014", e.message); fail++; }
}
function assert(cond, msg) { if (!cond) throw new Error(msg || "assertion failed"); }
test("rotates through servers in order", () => {
const lb = new LoadBalancer(["a", "b", "c"]);
assert(lb.nextServer() === "a");
assert(lb.nextServer() === "b");
assert(lb.nextServer() === "c");
assert(lb.nextServer() === "a"); // wraps
});
test("skips an unhealthy server", () => {
const lb = new LoadBalancer(["a", "b", "c"]);
lb.markUnhealthy("b");
const picks = [lb.nextServer(), lb.nextServer(), lb.nextServer()];
assert(!picks.includes("b"));
});
test("returns null when all servers are down", () => {
const lb = new LoadBalancer(["a", "b"]);
lb.markUnhealthy("a");
lb.markUnhealthy("b");
assert(lb.nextServer() === null);
});
test("a recovered server rejoins the rotation", () => {
const lb = new LoadBalancer(["a", "b"]);
lb.markUnhealthy("a");
lb.markHealthy("a");
const picks = [lb.nextServer(), lb.nextServer()];
assert(picks.includes("a") && picks.includes("b"));
});
console.log(`\n${pass} passed, ${fail} failed`);
5. The Interface
node balancer.mjs [ 'server1', 'server2', 'server3', 'server1' ] [ 'server3', 'server1', 'server3', 'server1' ]
6. Run It
node balancer.mjs
node balancer.test.mjs🎯 Try this next
- Add "least connections" balancing — send to the server with fewest active requests
- Add weighted round-robin so beefier servers get more traffic
- Add automatic health checks that ping servers on a timer
- Track per-server request counts and print a distribution report
What you practised
Round-robin selection with a wrapping pointer, routing around failures with a per-server health map, and the give-up-after-a-full-loop guard — the core of every real load balancer.
Reference: Maps · Control Flow · Classes