Design problems
Classic building blocksMediumSDE-2 25 min

Design a unique ID generator (like Snowflake)

Asked at
AmazonFlipkartPhonePeRazorpayUberZerodha

The interviewer asks

“Design a service that generates unique 64-bit IDs for orders, payments and messages across many data centres. IDs should be roughly sortable by time, and the system must generate hundreds of thousands of IDs per second with very low latency.”

What a strong answer looks like

A strong answer first pins down requirements: must IDs fit in 64 bits, must they be strictly increasing or only roughly time-ordered, and may they leak information (counts, time)? Then compare options: database auto-increment, UUIDv4, UUIDv7/ULID, ticket servers, range leasing, and Twitter Snowflake. Choose Snowflake-style IDs: 41 bits of milliseconds since a custom epoch, 10 bits of worker ID, 12 bits of sequence, generated locally in each service with no network call. The real depth is in the edge cases: assigning worker IDs safely (lease in etcd/ZooKeeper), clocks going backwards (NTP step, VM migration), sequence overflow within one millisecond, and the 69-year lifetime of the timestamp bits. Senior candidates also discuss B-tree index locality, why UUIDv4 primary keys hurt databases, and why IDs must not be used as secret tokens.

Concepts to mention

Snowflake bit layout: 41 timestamp, 10 worker, 12 sequencecustom epoch and 69-year lifetimeworker ID assignment via leasesclock skew and clocks going backwardssequence overflow handlingk-sorted vs strictly orderedUUIDv4 vs UUIDv7/ULID index localityno network call per IDJavaScript 2^53 precision issue

1 Requirements

Functional

  • Generate a unique 64-bit integer ID on request
  • IDs are roughly ordered by creation time (k-sorted): an ID created a second later is larger
  • Support batch generation (get 100 IDs in one call)
  • Extract the creation time from an ID (useful for debugging and partitioning)
  • Work across multiple regions and data centres with no coordination per ID

Non-functional

  • Uniqueness is absolute: a duplicate ID is a correctness bug (double payments, overwritten orders)
  • Latency: under 1 ms p99 (ideally in-process, ~100 ns)
  • Throughput: 500k IDs/s across the fleet; at least 4,000 IDs/ms per generator
  • Availability: ID generation must keep working even if the coordination service is down for minutes
  • Lifetime: the scheme must work for at least 50 years

Out of scope (say it out loud)

  • Human-friendly short codes (see URL shortener)
  • Strict global ordering across data centres (needs consensus; discuss why it is expensive)
  • Cryptographically secret tokens

2 Estimates

Timestamp bits41 bits ≈ 69.7 years2^41 ms = 2.199 × 10^12 ms ÷ (1000 × 86,400 × 365.25) ≈ 69.7 years from a custom epoch (e.g. 2025-01-01)
Worker bits10 bits = 1,024 generatorsCan split as 5 bits datacenter (32) + 5 bits machine (32), or 3 region + 7 worker
Sequence bits12 bits = 4,096 IDs per ms per worker4,096 × 1,000 = ~4.1M IDs/s per worker; fleet capacity 1,024 × 4.1M ≈ 4.2B/s
Fleet need500k IDs/s (peak 2M/s on sale days)Given; a few hundred workers each at a tiny fraction of capacity
UUIDv4 cost comparison128 bits, random order16 bytes vs 8 bytes per key and per index entry; random inserts touch random B-tree pages, causing page splits and poor cache hit rates

3 API

EndpointWhat it does
LibraryIdGenerator.next() -> int64In-process generation; the preferred path (no network).
gRPCIdService.Next(count) -> int64[]For services in languages without the library, or very small services; returns a batch.
LibraryIdGenerator.parse(id) -> {timestamp, worker_id, sequence}Debugging, time-based partition routing, TTL decisions.
Internaletcd lease /id-workers/{worker_id}Worker acquires and keeps alive a unique worker ID lease.

4 Data model

ID layout (64-bit)In memory (the ID itself)
1 bit sign (0) | 41 bits ms since custom epoch | 10 bits worker_id | 12 bits sequence

Positive signed int64, so it fits a BIGINT column and Java long. Sorted by time first, then worker, then sequence.

Worker leasesetcd / ZooKeeper
key /id-workers/{0..1023}value = host/pod idlease TTL 30 s

Each generator claims a free worker ID with a create-if-absent transaction and keeps the lease alive. Strong consistency is required here, and only here.

Last timestamp checkpointLocal disk or etcd
worker_idlast_ms_used

Written every few seconds; on restart the generator waits until the clock is past the checkpoint, protecting against clocks that went backwards during the restart.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAnext() in-processNext(count)claim worker idclaim worker idcheckpoint last msslew clockinsert with IDOrder / paymentservicesInternal LBID service (gRPC)In-process ID libraryNTP / chrony time syncetcd worker leasesOrders DB (BIGINT PK)Local last-mscheckpoint
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

Startup of a generator

  1. 1The pod starts and reads its last checkpointed timestamp from local disk (if any).
  2. 2It tries to create /id-workers/{n} in etcd with a 30 s lease for n = 0..1023 (starting at a random n) until one succeeds.
  3. 3It starts a keep-alive loop that renews the lease every 10 s.
  4. 4It waits until the current clock is at least last_checkpoint + a safety margin before issuing IDs.
  5. 5If the lease is ever lost (network partition), it stops generating IDs immediately and re-claims a worker ID.

Generate an ID

  1. 1next() reads the current time in ms (now) under a lock or with a compare-and-swap on a packed state.
  2. 2If now > last_ms: set sequence = 0, last_ms = now.
  3. 3If now == last_ms: sequence = sequence + 1; if it overflows 4,095, spin until the next millisecond.
  4. 4If now < last_ms (clock went back): if the gap is small (< 5 ms) wait it out; otherwise keep using last_ms as the logical time, or refuse and alert.
  5. 5Return ((last_ms - epoch) << 22) | (worker_id << 12) | sequence.

7 Deep dives

Comparing the options

Database auto-increment is simple and strictly ordered, but one database becomes a bottleneck and single point of failure, and with multiple primaries you need tricks like odd/even offsets that do not scale. A ticket server (Flickr's design: a MySQL table using REPLACE INTO to bump a counter, two servers with different offsets) works but adds a network hop per ID. Range leasing (a service hands out blocks of 10,000 IDs) removes the per-ID hop, but IDs are only ordered within a block. UUIDv4 needs no coordination at all, but is 128 bits, random, and bad for B-tree primary keys: inserts land on random pages, causing page splits, write amplification and low cache hit rates; MySQL InnoDB, which clusters rows by primary key, suffers most. UUIDv7 (RFC 9562, 2024) and ULID put a millisecond timestamp first, fixing index locality, but are still 128 bits. Snowflake gives 64-bit, time-ordered, coordination-free IDs: generation is a few hundred nanoseconds of local CPU. Choose Snowflake when you need 64-bit integers and high throughput; choose UUIDv7 when 128 bits is fine and you want zero setup.

Assigning worker IDs safely

Uniqueness depends entirely on no two live generators sharing a worker ID at the same millisecond. Static config (worker ID in a config file) breaks with autoscaling and Kubernetes pods that come and go. Using the last bits of an IP address can collide across subnets or after IP reuse. The safe approach is a lease in a strongly consistent store such as etcd or ZooKeeper: create /id-workers/17 only if absent, attach a 30 s lease, and renew every 10 s. If the generator loses contact with etcd and its lease might have expired, it must stop issuing IDs before the lease TTL passes (fencing by time), because another pod may have taken worker 17. To reduce reliance on etcd, generators keep their worker ID across restarts on the same node, and use etcd only at startup and for renewal; a short etcd outage does not stop generation as long as the lease has not expired. With 1,024 IDs available and a few hundred pods, there is room; if you need more, take bits from the sequence (e.g. 12 worker bits, 10 sequence bits = 1,024 IDs/ms/worker).

Clock problems

Snowflake trusts the system clock, and clocks misbehave. NTP can step the clock backwards after a large drift, a VM can pause and resume, and leap seconds can repeat a second on some systems. If the clock goes back 10 ms and the generator keeps going, it can reissue timestamps it already used, and with a reset sequence it can create duplicate IDs. Defences: configure chrony/NTP to slew (adjust gradually) rather than step; keep last_ms in memory and never issue an ID with a timestamp lower than last_ms; for small backward jumps, wait; for large ones, either keep using last_ms with an incrementing sequence (borrowing future time) or stop and alert. Persist last_ms periodically so a restart after a clock reset does not reuse time. Use a monotonic clock source for measuring progress plus wall-clock for the initial value. Also note that cross-machine ordering is only as good as clock sync, usually within a few ms, so IDs are k-sorted, not strictly ordered: never use them to decide which of two events in different services came first.

8 Trade-offs

In-process library vs central ID service

A library has no network hop and no extra service to run, but needs worker ID management in every service and versions in every language. A central gRPC service is easier to govern and change, but adds ~0.5-1 ms and becomes critical infrastructure; batching (100 IDs per call) and client-side buffers reduce the cost. Many companies use the library for core services and the service for the long tail.

64-bit Snowflake vs 128-bit UUIDv7

Snowflake halves key size (8 vs 16 bytes, which matters across billions of rows and every secondary index) and fits JavaScript only up to 2^53 safely, so APIs should send it as a string. UUIDv7 needs no worker IDs at all and has 74 random bits, so collisions are practically impossible without coordination. If your team cannot safely run worker leases, UUIDv7 is the safer choice.

Information leakage

Time-ordered IDs reveal creation time, and sequential patterns reveal business volume (competitors can estimate orders per day). That is usually acceptable for internal IDs; for public-facing IDs, expose a separate random or encrypted identifier, and never use any ID as an access token.

9 Bottlenecks

Single hot generator

If one ID service instance serves a big batch job, it can hit 4,096 IDs/ms and spin. Spread callers across instances, use batches, or give hot services their own in-process generators. In practice few systems reach this limit.

Worker ID exhaustion

Frequent pod churn with slow lease expiry can use up 1,024 IDs. Use shorter leases, reuse IDs per node, or reallocate bits.

10 Failure handling

etcd unavailable

Running generators continue while their lease is valid, and can keep generating safely as long as they stop before a possible lease expiry. New pods cannot start generating until etcd returns; keep a warm pool of running generators so scale-out is not blocked.

Clock jumps back by seconds

The generator detects now < last_ms, refuses to issue IDs (or uses logical time), fires an alert and removes itself from the load balancer. Callers retry on another instance. A few failed requests are far better than duplicate payment IDs.

11 What impresses

  • Does the bit math (69 years, 4,096 per ms) and adjusts bits to requirements
  • Explains why random UUID primary keys hurt B-tree indexes
  • Handles clock rollback explicitly instead of assuming NTP is perfect
  • Uses etcd leases with self-fencing for worker IDs
  • Mentions UUIDv7 (RFC 9562) as a modern alternative
  • Warns that IDs leak volume and time and must not be used as secrets

12 Mistakes

  • A single central database counter for all IDs
  • Hard-coding worker IDs in config with autoscaling pods
  • Ignoring what happens when the clock goes backwards
  • Claiming Snowflake IDs give strict global ordering
  • Choosing UUIDv4 as a clustered primary key without discussing the cost

13 Follow-ups

Q1How would you change the layout to support 10,000 generators?
Q2What happens to IDs after the 69 years run out?
Q3How do you guarantee strictly increasing IDs for a ledger?
Q4How would you use the ID for time-based sharding or partitioning?
Q5Why might a frontend show wrong IDs, and how do you fix it?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free