Tier 5 — Expert

Load Balancer

Distribute requests across a pool of servers with round-robin selection and health checks — the traffic cop in front of every scaled system.

🐍 Python 🟨 JavaScript

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

balancer.mjs
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

balancer.test.mjs
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

  1. Add "least connections" balancing — send to the server with fewest active requests
  2. Add weighted round-robin so beefier servers get more traffic
  3. Add automatic health checks that ping servers on a timer
  4. 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

Try It Yourself