• Spin up 3 postgres instances with identical schema
    • 5434, 5435, 5436 (host ports; must match the ports used in index.js)
  • Write
  • Read

1. Create init.sql

CREATE TABLE URL_TABLE
(
  id serial NOT NULL PRIMARY KEY,
  URL text,
  URL_ID character(5) UNIQUE
);

2. Dockerfile

FROM postgres
COPY init.sql /docker-entrypoint-initdb.d

3. Docker Image

docker build -t pgshard .

4. Docker containers

# Each container needs a unique name, a unique host port and a password
# (the official postgres image won't start without POSTGRES_PASSWORD or an auth method)
docker run --name pgshard1 -e POSTGRES_PASSWORD=postgres -p 5434:5432 -d pgshard
docker run --name pgshard2 -e POSTGRES_PASSWORD=postgres -p 5435:5432 -d pgshard
docker run --name pgshard3 -e POSTGRES_PASSWORD=postgres -p 5436:5432 -d pgshard

Node server

// index.js
 
const express = require("express");
const { Client } = require("pg");
// const ConsistentHash = require("consistent-hash");
const HashRing = require("hashring");
const crypto = require("crypto");
 
const app = express();
app.use(express.json());
 
// const hr = new ConsistentHash();
const hr = new HashRing();
hr.add("5436");
hr.add("5435");
hr.add("5434");
 
const clients = {
  5436: new Client({
    host: "localhost",
    port: "5436",
    user: "postgres",
    password: "postgres",
    database: "postgres",
  }),
  5435: new Client({
    host: "localhost",
    port: "5435",
    user: "postgres",
    password: "postgres",
    database: "postgres",
  }),
  5434: new Client({
    host: "localhost",
    port: "5434",
    user: "postgres",
    password: "postgres",
    database: "postgres",
  }),
};
 
// Connect to all PostgreSQL instances
async function connectAllClients() {
  for (const port in clients) {
    try {
      await clients[port].connect();
      console.log(`Connected to Postgres on port ${port}`);
    } catch (err) {
      console.error(`Failed to connect to Postgres on port ${port}:`, err);
    }
  }
}
connectAllClients();
 
app.get("/:urlId", async (req, res) => {
  const urlId = req.params.urlId;
  const server = hr.get(urlId);
  const results = await clients[server].query(
    "SELECT * FROM URL_TABLE WHERE URL_ID = $1",
    [urlId],
  );
 
  if (results.rowCount > 0) {
    originalUrl = results.rows[0].url;
    res.send({
      originalUrl: originalUrl,
    });
  } else {
    res.sendStatus(404);
  }
});
 
app.post("/", async (req, res) => {
  const url = req.query.url;
  const hash = crypto.createHash("sha256").update(url).digest("base64url"); // base64url: no "/" or "+" in the URL path
  const urlId = hash.substr(0, 5);
 
  const server = hr.get(urlId);
 
  await clients[server].query(
    "INSERT INTO URL_TABLE (URL, URL_ID) VALUES ($1, $2)",
    [url, urlId],
  );
 
  res.send({
    hash: hash,
    urlId: urlId,
    server: server,
  });
});
 
app.listen(8081, () => console.log("Listening on port 8081"));
  • Collisions: 5 base64url chars = 64⁵ ≈ 1 billion ids, so different URLs can collide. The UNIQUE on URL_ID makes a collision fail loudly instead of silently storing two URLs under one id (each id always routes to the same shard, so a per-shard UNIQUE is enough). Re-posting the same URL also hits it; handle the error or look the id up first