Requirements & API: Unique ID Generator (Snowflake)

The first move in any interview: define requirements and sketch the API before drawing a single box.

Functional requirements

  • Generate globally unique 64-bit IDs across many machines with no collisions.
  • IDs should be roughly sortable by creation time (k-sorted), so newer IDs are numerically larger.
  • Mint IDs at very high throughput from each node without a per-request network round trip.
  • Assign each generator node a unique worker id at startup so two nodes never produce the same ID.

Non-functional requirements

  • No single point of contention on the hot path: ID generation must not require coordination per request.
  • Low latency: an ID is produced locally in microseconds.
  • Numeric, compact (64-bit fits a BIGINT) and time-ordered enough to index well.
  • Tolerate clock skew safely: never issue an ID that could duplicate an earlier timestamp+sequence.

API contract

nextId() → int64
Local call on a generator node. Composes timestamp | worker_id | sequence into 64 bits.
(startup) registerWorker() → worker_id
One-time call to the coordination service to claim a unique 10-bit worker id.

About Unique ID Generator (Snowflake)

Every row in a sharded database, every order, every message needs an id, and at scale you cannot just use a single auto-incrementing column. The moment your data lives on more than one machine, one shared counter becomes the bottleneck that every write has to wait behind. A distributed unique ID generator solves this, and the canonical answer (Twitter's Snowflake) is a beautiful little piece of bit-packing that interviewers love because the whole design fits in 64 bits.

Here is the whole thing in plain terms. An app server needs an id, so it asks a local ID generator, which hands one back immediately without talking to any other node. The id is a 64-bit number split into fields: one unused sign bit, a 41-bit millisecond timestamp, a 10-bit worker id, and a 12-bit sequence number. The timestamp goes in the high bits, so ids minted later are numerically larger, which makes them roughly sortable by time. The worker id guarantees two different machines never collide even in the same millisecond. The sequence counts ids minted within a single millisecond on one worker, so a single node can mint 4,096 ids per millisecond.

The one idea worth getting straight is why this needs almost no coordination. A worker only talks to the coordination service (ZooKeeper or etcd) once, at startup, to claim a unique worker id. After that it mints ids entirely on its own using its local clock and an in-memory counter. That is the difference between this and a shared database counter: there is no per-request round trip, so a worker can mint millions of ids per second.

The trade-offs are real and worth naming. The design leans on the wall clock, so if NTP steps the clock backward the worker must refuse to issue ids until time catches up, or it would risk repeating a timestamp. And 41 bits of milliseconds only covers about 69 years from your chosen epoch, while 10 bits caps you at 1,024 workers. This system teaches why a single auto-increment column does not scale, the UUID-versus-ticket-server-versus-Snowflake trade-off, bit-packing as a design tool, and why clock skew is the sharp edge of any timestamp-based id.