Design a unique ID generator
Design the service (or library) that gives every new order, message and photo a unique ID: a million IDs a second across three regions, no two ever the same, roughly in time order, and fitting in 64 bits so database indexes stay small. Module 20 explains the ID formats; this design is about running them: who hands out machine numbers, what happens when a clock jumps backwards, and when to avoid clocks altogether.
1. The prompt
"Design a system that generates unique IDs for a large distributed application."
What makes it hard:
- No central counter. One database sequence is unique and ordered, but every ID would wait for one machine in one region.
- Clocks lie. Time-based IDs are sortable for free, but clocks drift, get corrected backwards, and differ between machines.
- 64 bits is a tight budget. Time, machine and sequence must share 63 usable bits, and each choice limits how long the scheme lasts or how big it can grow.
2. Frame the problem
Must IDs be sortable by time?
Roughly: sorting by ID should give creation order to within a second or so. Exact global order isn't needed.
"Roughly" allows each machine to use its own clock; exact order would need coordination (Module 20).
How big can an ID be?
64 bits: it's the primary key of most tables and appears in every index and URL.
Rules out UUIDs (128 bits) as the main key; they stay an option elsewhere.
How many, where?
Peak 1 million IDs a second across 3 regions and about 2,000 application servers.
Decides how many machine numbers and how many IDs per millisecond each needs.
Will IDs be public?
Yes, in URLs (/orders/7203…).
Time-based IDs reveal creation time and volume; decide whether that matters (9.5).
How long must the scheme last?
At least 50 years.
Decides how many bits time gets, and the custom start date (epoch).
3. What it must do
- Hand out IDs that are unique across every machine and region, forever.
- Keep them roughly time-ordered, so new rows land at the end of indexes.
- Keep working when a region is cut off or the coordination service is down.
Not now: short human-friendly codes (that's D01), strictly gap-free numbering (invoices need a separate, per-company sequence for legal reasons).
4. How well it must do it
| Target | Why this number |
|---|---|
| Zero duplicates, ever data safety | A duplicate primary key loses data or fails a write in a way nobody expects. |
| Under 1 µs per ID, with no network call speed | Generating an ID must never be the slow part of a write. |
| Available as long as the app is surviving failures | If IDs stop, every insert stops. |
| Sortable within ~1 s across machines consistency | Good enough for "newest first" and for index locality. |
| Lasts 50+ years evolution | Changing the ID scheme of a live system is a long migration. |
5. Numbers that change the design
| Quantity | Working | → so we… |
|---|---|---|
| Time | 241 ms ≈ 69.7 years | → so 41 bits of milliseconds from a custom epoch (e.g. 1 Jan 2026) last until 2095 |
| Machines | 2,000 servers today, maybe 10,000 later; 210 = 1,024 is too few | → so either 13 machine bits, or IDs come from a few hundred generator processes, not every app server |
| Per machine per ms | 1 M/s ÷ 2,000 = 500/s = 0.5 per ms on average; bursts of 100× on one machine | → so 10–12 sequence bits (1,024–4,096 per ms) is plenty |
| Central counter | 1 M/s through one database sequence, cross-region round trips of 70–150 ms | → so no per-ID coordination; at most per-block (9.3) |
The lab in section 13 lets you move the bits around and see the trade-offs.
6. The nouns
- ID: 1 sign bit (always 0) · 41 bits of milliseconds since our epoch · 12 bits of generator number · 10 bits of sequence.
- Generator: a process allowed to mint IDs, holding a unique generator number under a lease.
- Lease: the right to use a generator number, renewed every few seconds in a coordination service (etcd, ZooKeeper).
id (64 bits) 0 | ms since 2026-01-01 (41) | generator (12) | sequence (10)
generator_lease /idgen/generators/{0..4095} → holder (host:pid), expires_at, last_ms_used (etcd)
per process last_ms, sequence (memory)
7. The contract
Library (preferred): id = idgen.next() // in-process, no network, ~100 ns
Service (alternative): POST /v1/ids?count=100 → [ids] (for clients that can't embed the library)
Decode (support tools): GET /v1/ids/{id} → {created_at, generator, sequence}
The library form keeps ID generation off the network entirely; the service form exists for languages or devices that can't hold a lease.
8. First working version
Each application process embeds the generator library. At start-up it claims a free generator number with a lease; after that, every ID is built from the clock, its number and a counter, entirely in memory.
8.1 A process starts and claims a number
8.2 Minting IDs in one millisecond
9. Make it hold up
9.1 When the clock goes backwards data safety
Run time sync that slews (speeds up or slows down the clock gradually) rather than stepping it, which makes backward jumps rare (Module 20). And never trust wall-clock time for correctness anyway: the "last millisecond used" check is what guarantees uniqueness.
9.2 Running out of sequence speed
With 10 sequence bits a generator can mint 1,024 IDs in one millisecond. The 1,025th waits until the next millisecond: at worst a sub-millisecond pause, about a million IDs a second per generator. If that's a real limit, give the busiest services more generator numbers, or more sequence bits and fewer generator bits. Never "borrow" from the next millisecond's numbers early without recording it, or a restart can reuse them.
9.3 IDs without clocks: handing out blocks surviving failures
| Approach | Why | |
|---|---|---|
| ✗ Bad | One database sequence, called for every ID | Simple and strictly ordered, but every insert in every region waits for one machine, and it's a single point of failure. |
| ~ Good | Random IDs (UUIDv4) | No coordination at all, but 128 bits, not time-ordered (new rows land all over the index, Module 12), and unreadable. |
| ✓ Great, depending on needs | Time-based 64-bit IDs (this design) where order matters; or blocks from a counter: each process reserves 10,000 numbers at a time from a database sequence and hands them out from memory | Blocks need no clock and stay 64-bit and dense, at the cost of order between processes; when the counter is down, processes keep going until their block runs out. Flickr ran this with two counters (odd and even numbers) for availability. |
9.4 Three regions surviving failures
Split the generator numbers by region (0–1,365 for region 1, and so on), so each region's coordination service hands out only its own range. A region cut off from the others keeps minting with no risk of collision, because no other region can hold its numbers. IDs from different regions are ordered by their clocks, which are within milliseconds of each other: "roughly sorted", as agreed.
9.5 IDs in public security
- They reveal time. Anyone can decode when an order was placed. Usually fine; avoid for things like account creation if that's sensitive.
- They reveal volume. Two orders a day apart show how many generators and sequence numbers were used in between, roughly. Strictly sequential IDs (1, 2, 3…) reveal it exactly.
- They're guessable. Never let an ID act as permission: check that the caller may see order 7203… (Module 26). Where guessing itself is a problem (shared links), use a random token alongside the ID.
9.6 The whole design after the deep dives
10. Interviewer follow-ups
Why not just use UUIDv7?
UUIDv7 (RFC 9562) is time-ordered and needs no coordination at all, because 74 random bits make collisions vanishingly unlikely. If 128-bit keys are acceptable, it's the simplest answer. We chose 64 bits for smaller indexes and keys; that budget is what forces generator numbers and leases.
Moving an existing system from auto-increment IDs.
Start the new scheme above the current maximum (the time component makes new IDs far larger than old ones), switch writers one service at a time, and keep both formats valid in parsers. Foreign keys don't change, since both are 64-bit integers.
What if etcd is down when a process starts?
The process can't claim a number, so it doesn't start serving writes (fail closed). Running processes keep their leases for a while; if renewal fails past the lease, they stop minting rather than risk a duplicate.
A VM is snapshotted and restored twice.
Both copies believe they hold the same generator number: duplicates. Leases help (only one copy can renew), but the restored copies must re-claim a number on resume before minting; the library detects a large jump in monotonic time and re-registers.
Client-generated IDs for offline apps?
Let devices create UUIDv7s (no coordination possible offline) as the client-side key, and map them to server IDs when synced, or use the UUID as the key for that table.
11. When things break
| Failure | What users see | How we detect it | What limits the damage |
|---|---|---|---|
| Clock steps backwards | A few ms of waiting on one host | Library metric: backward jumps | Refuse until the clock passes the last used ms |
| Coordination service down | New processes can't start | Lease errors | Running processes continue within their lease |
| Lease lost while running | That process stops minting | Renewal failures | Fail closed; restart claims a new number |
| Generator numbers exhausted | New processes can't start | Free-number gauge | Alert at 80%; free numbers of dead processes after lease expiry |
| Duplicate key on insert | A write fails | Unique-key violations | Investigate immediately: it means a broken assumption |
12. Wrap-up and the clock
Trade-offs we made:
- 64-bit time-based IDs: small and roughly ordered, at the cost of generator numbers and leases.
- A library, not a service: no network call per ID.
- Refuse to mint when the clock goes back, rather than risk a duplicate.
- Generator numbers split by region, so partitions can't cause collisions.
What we'd watch: backward clock jumps · sequence exhaustion waits · free generator numbers · lease renewal errors · any duplicate-key error.
At 10×: more generator numbers (a bit from the sequence), or IDs minted by fewer, busier processes. The format must be decided once, so leave room up front.
A 45-minute interview
| 0–5 | Frame: size, order, volume, lifespan |
| 5–12 | Options: sequence, UUID, blocks, time-based |
| 12–22 | The bit layout, with the numbers |
| 22–38 | Deep dives: generator numbers, clock going back (9.1), regions (9.4) |
| 38–45 | Public IDs, follow-ups, wrap-up |
A 60-minute interview
| 0–6 | Frame and numbers |
| 6–14 | Options and the choice |
| 14–24 | First version: claim a number, mint |
| 24–50 | Deep dives 9.1–9.5 |
| 50–60 | Follow-ups, trade-offs, 10× |
| Level | What a strong answer shows |
|---|---|
| Mid-level | Knows the options (sequence, UUID, Snowflake) and their trade-offs; a sensible bit layout. |
| Senior | Bit budget from the numbers, generator numbers via leases, clock-backwards handling, sequence exhaustion, library vs service. |
| Staff+ | Regions and partitions, VM snapshots, migration from an old scheme, public-ID leakage, when 128-bit UUIDv7 is the better answer. |
Common mistakes:
- Hard-coding machine numbers in config, then two copies of one config start.
- Ignoring a clock that goes backwards.
- A network call to an ID service for every ID.
- Treating an unguessable ID as an access check.
13. Try it, check yourself, read more
The bit-budget lab. 63 bits to share between time, generator number and sequence. Move the sliders and see how long the scheme lasts, how many generators it allows and how fast each can mint.
Sequence bits are what's left of 63 (bit 64 is the sign, kept 0 so IDs are positive in every language). Targets from section 2: 50 years, 2,000–10,000 generators, bursts of a few hundred IDs per millisecond per generator.
A generator's clock jumps back by 5 ms. What should it do?
How should a generator get its unique number?
When is UUIDv7 the simpler choice?
| Resource | Why |
|---|---|
| Twitter Snowflake (2010) | The original 41 + 10 + 12 layout. |
| Discord API reference: Snowflakes | A live variant, with 42 time bits and a 2015 epoch. |
| Flickr: ticket servers | IDs from counters, with two servers for availability. |
| RFC 9562: UUIDs, including version 7 | The 128-bit time-ordered alternative. |