Skip to the solution
HardSystem Design14 min read

How would you design a unique ID generator at scale?

0 views

The problem

Question presented to candidate: "Every tweet, order and message in our system needs an ID. We write about 20,000 rows a second across many database shards, peaks are ten times that, and we want IDs that are unique without a central bottleneck and roughly sortable by creation time. How would you generate them?"

What a strong answer should cover:

  • Requirements: uniqueness across machines, size (64-bit fits a BIGINT, 128-bit does not), time-sortable (k-sorted) or not, throughput, no single point of failure, whether IDs may leak information (volume, creation time)
  • Options and trade-offs: database auto-increment, ticket servers (Flickr), range allocation, UUIDv4 (random, 128-bit, poor index locality), UUIDv7 (RFC 9562, time-ordered, 128-bit), Snowflake (64-bit: timestamp + machine + sequence)
  • Snowflake bit math: 41 bits of milliseconds last about 69 years from a custom epoch, 10 bits give 1,024 workers, 12 bits give 4,096 IDs per ms per worker
  • Hard parts: sequence overflow within a millisecond (wait for the next ms), clock moving backwards (refuse or wait), unique worker ID assignment (config, or leases from a coordination service), and IDs only k-sorted across machines
  • Practical details: IDs above 2^53 lose precision in JavaScript, so APIs send them as strings

Clarifying questions expected:

  • Must IDs be 64-bit integers, or is a 128-bit value acceptable?
  • Do they need to be sortable by time, and is it acceptable that they reveal creation time?
  • How many generating machines, and how is a machine identity assigned?

Code / implementation expected: Yes โ€” a Snowflake-style generator; the example runs one on a fake clock through sequence overflow, a clock rollback and a duplicate worker ID.

  • system-design
  • case-study

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

14 min read ยท 16 sections

Read the code

Snowflake IDs on a fake clock: layout, sequence overflow, clock going backwards, ordering, duplicate worker IDs
// A Snowflake-style 64-bit ID generator on a fake clock.
// Layout: 1 unused sign bit | 41 bits ms since a custom epoch | 5 datacenter | 5 worker | 12 sequence
const EPOCH = 1288834974657n; // the Twitter epoch, Unix ms
const SEQ_BITS = 12n, WORKER_BITS = 5n, DC_BITS = 5n;
const MAX_SEQ = (1n << SEQ_BITS) - 1n;

class FakeClock {
  constructor(ms) { this.ms = ms; this.waits = 0n; }
  now() { return this.ms; }
  sleepUntil(ms) { this.waits += ms - this.ms; this.ms = ms; }
}

class Snowflake {
  constructor(dc, worker, clock, onBackwards) {
    this.dc = BigInt(dc); this.worker = BigInt(worker); this.clock = clock;
    this.onBackwards = onBackwards; this.last = -1n; this.seq = 0n;
  }
  next() {
    let ts = this.clock.now();
    if (ts < this.last) {
      if (this.onBackwards === "refuse") throw new Error("clock moved back " + (this.last - ts) + " ms, refusing");
      this.clock.sleepUntil(this.last); // wait for the clock to catch up
      ts = this.last;
    }
    if (ts === this.last) {
      this.seq = (this.seq + 1n) & MAX_SEQ;
      if (this.seq === 0n) { this.clock.sleepUntil(this.last + 1n); ts = this.last + 1n; } // 4096 used this ms
    } else this.seq = 0n;
    this.last = ts;
    return ((ts - EPOCH) << 22n) | (this.dc << 17n) | (this.worker << 12n) | this.seq;
  }
}

function decode(id) {
  return { ms: (id >> 22n) + EPOCH, dc: (id >> 17n) & 31n, worker: (id >> 12n) & 31n, seq: id & MAX_SEQ };
}

console.log("== 1. three IDs from datacenter 1, worker 3");
const clock = new FakeClock(1760000000000n);
const gen = new Snowflake(1, 3, clock, "wait");
for (const step of [0n, 0n, 1n]) {
  clock.ms += step;
  const id = gen.next();
  const d = decode(id);
  console.log("  " + id + "  ms=" + d.ms + " dc=" + d.dc + " worker=" + d.worker + " seq=" + d.seq);
}

console.log("== 2. 5000 IDs requested while the clock is frozen");
{
  const c = new FakeClock(1760000000100n);
  const g = new Snowflake(1, 3, c, "wait");
  const perMs = new Map();
  let prev = -1n, increasing = true;
  for (let i = 0; i < 5000; i++) {
    const id = g.next();
    if (id <= prev) increasing = false;
    prev = id;
    const ms = decode(id).ms;
    perMs.set(ms, (perMs.get(ms) || 0) + 1);
  }
  for (const [ms, n] of perMs) console.log("  ms " + ms + ": " + n + " IDs");
  console.log("  generator waited " + c.waits + " ms, strictly increasing: " + increasing);
}

console.log("== 3. the clock steps back 5 ms (an NTP correction)");
for (const policy of ["refuse", "wait"]) {
  const c = new FakeClock(1760000000200n);
  const g = new Snowflake(1, 3, c, policy);
  const a = g.next();
  c.ms -= 5n;
  try {
    const b = g.next();
    console.log("  " + policy.padEnd(7) + "next ID " + b + " > previous " + a + ": " + (b > a) + ", waited " + c.waits + " ms");
  } catch (e) {
    console.log("  " + policy.padEnd(7) + "error: " + e.message);
  }
}

console.log("== 4. two workers, interleaved over 3 ms, then sorted by ID");
{
  const c = new FakeClock(1760000000300n);
  const w1 = new Snowflake(1, 1, c, "wait"), w2 = new Snowflake(1, 2, c, "wait");
  const ids = [];
  for (let ms = 0; ms < 3; ms++) {
    ids.push(["w2", w2.next()], ["w1", w1.next()]);
    c.ms += 1n;
  }
  ids.sort((x, y) => (x[1] < y[1] ? -1 : 1));
  console.log("  " + ids.map(([w, id]) => w + "@" + (decode(id).ms - 1760000000000n)).join(" "));
}

console.log("== 5. two processes misconfigured with the same worker ID");
{
  const c = new FakeClock(1760000000400n);
  const p1 = new Snowflake(1, 7, c, "wait"), p2 = new Snowflake(1, 7, c, "wait");
  const seen = new Set();
  let dupes = 0;
  for (let i = 0; i < 1000; i++) {
    for (const p of [p1, p2]) { const id = p.next(); if (seen.has(id)) dupes++; seen.add(id); }
    if (i % 100 === 99) c.ms += 1n;
  }
  console.log("  2000 IDs generated, duplicates: " + dupes);
}

console.log("== 6. limits of this layout");
const maxMs = (1n << 41n) - 1n;
console.log("  41-bit timestamp lasts " + maxMs / 31557600000n + " years, until Unix ms " + (EPOCH + maxMs));
console.log("  per worker: " + (MAX_SEQ + 1n) * 1000n + " IDs/s; workers: " + (1n << (DC_BITS + WORKER_BITS)));
console.log("  largest ID " + ((maxMs << 22n) | (1n << 22n) - 1n) + " fits a signed 64-bit integer: " + (((maxMs << 22n) | (1n << 22n) - 1n) <= 9223372036854775807n));

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 95 of 106 in System design

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

Up nextHard

How would you design a real-time gaming leaderboard?

Continue