System-design interview · Extended interviews
Design a distributed unique-ID generator
Allocate unique integer IDs with a database baseline, then reserve durable numeric ranges so local counters can serve high throughput without per-ID coordination.
You will learn to
- Separate unique allocation from business retries and strict ordering.
- Scale a durable database allocator with disjoint numeric ranges and synchronized local counters.
- Explain gaps, restart behavior, exact integer transport and the alternatives when requirements change.
Practice in this chapter
8 interview questions with model answers and follow-ups.
Go to interview practiceUseful foundations: Capacity estimation: throughput, latency, concurrency and storage · Quorums, consensus, leases, and fencing · CAP theorem: consistency, availability, and partition tolerance · Databases, data models, and ACID transactions
Workload and timing examples are interview assumptions.
Dotted concept links open the relevant explanation in a new tab.
01Ask what the identifier must guarantee
An ID generator provides distinct values that other services use as record keys. Ask which namespace needs uniqueness, whether IDs must be integers, whether gaps are acceptable, and whether values must reveal creation order. Those are separate requirements. A unique value does not prove a record was committed, does not authorize access and does not make a retried business request safe.
Our order services need positive integers before batching database writes. Gaps are acceptable. IDs need not follow strict global real-time order and do not need to encode time. These choices let us build a straightforward allocator rather than begin with a custom clock-based format. Keep a unique constraint in the order database as a final detector of a violated assumption.
The complete flow is simple: the order service asks an allocator for an ID, receives a safely allocated value, and then uses it in its own order transaction. If that transaction’s reply disappears, the order API uses a separate business request key to discover its result. Asking for another ID would not establish that the original order failed.
02Functional requirements
Agree on what the service must do before choosing its components.
- Allocate identifiers. Issue positive integer IDs within a named namespace for consuming services to use as record keys.
- Support bounded allocation. Offer individual IDs and bounded batches, with explicit unavailable or exhaustion responses rather than wrapping into previously used values.
- Inspect namespace state. Allow authorized inspection of allocation state and capacity. Namespace creation and migration are privileged, audited operations.
Gapless invoice numbering, secret access tokens, exact creation timestamps and business-request idempotency are outside the generator contract.
03Non-functional requirements
Use these hypothetical requirements for the worked interview. Confirm the assumptions with the interviewer; the numerical targets require testing and are not measured results. For latency, p95 and p99 mean that 95% and 99% of measured delays, respectively, are no greater than the reported value.
- Workload. Plan for ten million IDs/s peak across 500 generator processes, or 20,000/s per process on average. Bursts and uneven distribution still require testing.
- Latency and availability boundary. Target local p99 below 100 microseconds while the process has unused IDs already reserved for it. Local issuance may continue during an authority outage until those IDs run out; replenishment then needs the authority, and exhausted processes must wait within a deadline or fail explicitly.
- Uniqueness and durable allocation. No successfully issued value in one namespace may be repeated under supported failures. Committed ranges and replay results must survive allocator failover; an asynchronous promotion that loses acknowledged allocation state does not meet this requirement. Unused values and non-global issue order are acceptable.
- Safe restart and recovery. A restarted process abandons its previous range. Never restore uncertain allocation history or clone one active in-memory cursor into simultaneous issuers. Arbitrary live-memory cloning requires a stronger external allocation boundary. Backups cannot silently move the allocated boundary backward.
- Exact representation and access. Preserve the full integer value across client types, for example with decimal strings where needed. Authenticate generators and namespace administration; a predictable ID never grants access to a business record.
04Allocate from one durable counter
Begin with one allocation service and an Allocator row:
| Field | Purpose |
|---|---|
namespace |
The ID space whose allocation is being coordinated. |
nextValue |
The next integer available for allocation in that namespace. |
A single-ID request follows this transaction:
- Lock. Open a short transaction and lock the namespace row.
- Reserve. Read its next available integer and increment
nextValue. - Commit. Preserve the advance with the required durability.
- Return. Send the allocated value only after that commit.
Concurrent requests take turns at this row, so they cannot receive the same value.
If nextValue is 1001, request A reserves 1001 and commits 1002 as the next value. Request B then gets 1002. If A crashes after commit but before replying, 1001 may never be used. That is an acceptable gap. It must not be recycled merely because the service did not observe the client using it.
A database sequence is another implementation option with its own documented caching and durability behavior. The essential contract is to preserve acknowledged allocation state across supported failures and never cycle into used values. A small deployment can stop here.
The allocation transaction and the order transaction remain separate. A user-visible order is created only when the order service commits it. This explains why the generator can produce gaps even during healthy operation: clients can abandon work or fail after allocation.
Allocation precedes, but does not commit, the business record.
05Calculate the value of batching coordination
At one remote allocation per ID, ten million/s is substantial control traffic. A serial caller with a one-millisecond round trip can issue roughly 1,000 requests/s. More concurrency improves aggregate rate, but adds network and database pressure while every allocation still depends on the authority.
Reserve a numeric range of 10,000 IDs per control transaction. At 20,000 IDs/s, each process needs roughly two ranges/s. Across 500 processes, the authority handles about 1,000 range transactions/s instead of ten million per-ID calls. Local increments handle the remaining work.
| Choice | Benefit | Cost |
|---|---|---|
| Larger range | Fewer authority calls and more outage runway | More unused IDs after a crash |
| Smaller range | Less abandoned space | More replenishment and coordination |
| Early prefetch | Hides ordinary replenishment latency | Additional committed unused ranges |
| Per-call central allocation | Simple shared ordering boundary | Network/authority dependency on each ID |
Size range requests with expected rate and acceptable outage duration. Benchmark local synchronization under bursts, not just arithmetic. Integer capacity is finite even when it is very large: define the maximum and reject a reservation whose end exceeds it. Do not silently wrap a counter or reuse another namespace’s space.
06Make range ownership and API identity explicit
Separate namespace allocation state from individual reservations:
| Record | Identity and fields | Purpose |
|---|---|---|
| Namespace allocator | Namespace; next-unallocated boundary | Preserve the durable allocation boundary. |
| Range reservation | Process incarnation and range request key; start and exclusive end | Record which consecutive values belong to that reservation. |
An incarnation identifies one start of a process. A replacement uses a new incarnation so it cannot claim the old process’s range.
Range request from a trusted generator
POST /ranges
{
"namespace": "orders",
"incarnation": "A9",
"requestId": "r17",
"count": 10000
}
Example reserved range
Interval: [1001, 11001)
First value: 1001
Last value: 11000
Exclusive end: 11001
A half-open interval includes the start and excludes the end. The following range can therefore start at 11001 without sharing an endpoint.
Authenticate the generator and bound requested count. Within the same live incarnation, repeating r17 returns the saved range; different count or namespace under r17 conflicts. The range record and boundary advance commit together. Install a returned reservation only once within the live incarnation. A duplicate response for r17 refers to the already installed range and its current cursor; it must never reset that cursor to the range start. A new incarnation must use new request identity and receive a fresh range, rather than recovering an old cursor after a crash.
Local nextId() returns one value from the current range or a clear unavailable/exhausted response. A remote batch API can additionally retain its returned batch under a request key, but that is a separate replay contract with storage and retention costs.
07Move only the fast counter into each process
Scale the authority operation from “reserve one” to “reserve count.” Perform these steps in one database transaction:
- Lock the namespace row. Read its current
nextValue. - Calculate the reservation. Set
start = nextValueandend = start + count. - Check capacity. Validate the bound before changing allocation state.
- Record both changes. Advance
nextValuetoendand insert the request/result record. - Commit, then return. Return the range only after commit.
Each process keeps a cursor and the exclusive end of its committed range. An atomic increment or short local lock reserves the next unused value. No network call is required while usable values remain. Near exhaustion, a background replenisher asks for another range and installs it under the same local synchronization rules.
The ranges are disjoint because the database serialized their boundary advances. Adding generator processes therefore increases aggregate local capacity without changing the central uniqueness argument. The authority still controls a small stream of range reservations.
Partition allocation by independent namespaces if the product already has them. Splitting one integer space among several uncontrolled allocators would require another disjointness rule and is unnecessary at the assumed control rate. First measure whether a replicated database can meet roughly 1,000 short range transactions/s with recovery headroom. Avoid adding a distributed custom protocol before that measurement shows a need.
The authority is called per range rather than per identifier.
Read each connection in order
- syncReserve disjoint ranges durablyRange authority → High-water and request results
- return[1001,11001)Range authority → Generator A / local cursor
- return[11001,21001)Range authority → Generator B / local cursor
- syncAtomic local nextIdGenerator A / local cursor → Generator A / local cursor
- syncAtomic local nextIdGenerator B / local cursor → Generator B / local cursor
08Trace local calls and the last value in a range
Generator A holds [1001,11001) with cursor 1001. Under a local lock, request 1 reads 1001, advances cursor to 1002 and returns 1001. Request 2 observes 1002 and returns it. Without synchronization, both threads could read 1001 before either advanced the cursor, duplicating a value despite perfectly disjoint cross-process ranges.
At cursor 11000, the process can return the last allowed value and advance to 11001. It cannot issue 11001 from this range. If a prefetched range is ready, atomically switch to its start; otherwise wait within the caller’s deadline or return unavailable. Never increment beyond the end and continue after an error.
A local batch reserves its whole requested span under the same guard. Decide whether a batch may cross into another already committed range or must return fewer values; document the response instead of assuming contiguity across separate reservations.
The order service then creates the order using the ID and its own stable business request key. If its response is lost, a retry finds that order’s saved result. A generation call lost before the caller receives it can waste an ID, but it must never cause the allocator to return that possibly delivered value again.
09Prove uniqueness through crashes and failover
Within a live process, synchronized cursor updates assign different values. Between processes, committed numeric ranges do not overlap. Those two facts establish the ordinary uniqueness proof without any wall-clock assumption.
If a generator crashes halfway through its range, restart abandons all remaining values and requests a fresh range. It does not need to recover the exact last local increment, because none of its old range is reused. This deliberately trades gaps for simpler crash safety. A paused process can resume using its existing range while a replacement has a different range; their values still differ.
If the allocator commits a range but loses its reply, the same running process retries with the same request identity and receives the saved range. Duplicate responses reuse an already installed reservation’s current cursor; they never reinstall it from the beginning. A new process must not take that old permission if the former process might still be alive. Restart therefore uses a new incarnation and a new range.
Allocator failover must retain committed boundaries and result records. A stale promoted replica or restored backup can otherwise allocate an overlapping historical range. Refuse service when that evidence cannot be established, or move to an explicitly different namespace. A downstream unique constraint can detect failure but is not permission to tolerate an unsafe allocator.
10Keep identifiers exact and order claims modest
Return large integer IDs as decimal strings when clients cannot represent the full integer range exactly. JavaScript Number is exact only through 2^53 − 1; rounding a valid larger ID can collapse distinct values at the client. Parse directly into an appropriate integer type and keep namespace information wherever values from different spaces can meet.
Numeric ranges do not provide global issue order. Process A may still issue 1002 after process B has issued 11001. That does not violate uniqueness. Store an explicit business createdAt when the application needs creation time, and use an actual sequencing protocol if global real-time order is required.
IDs can reveal rough activity or be predictable. Authorization checks must verify the actor and record owner. If public URLs need to conceal internal ordering, store a separate random reference rather than treating the integer as a secret.
Do not reset the counter when deploying a new version or restoring a test dataset into production. Namespace creation and migration are privileged audited actions. Mixing two independently allocated spaces as bare integers loses the uniqueness scope that made each one correct. API documentation must state that scope as clearly as the wire format.
11Explain when the simpler design stops fitting
If clients need decentralized allocation without a control service, an established UUID implementation may be preferable. UUIDv4 provides probabilistic uniqueness from correctly generated randomness. UUIDv7 is a standard timestamp-leading format that improves time locality without promising strict global real-time order. Neither should be casually truncated into a smaller integer.
A Snowflake-style format combines timestamp, worker and per-timestamp sequence fields. It can provide compact approximate time ordering, but now worker reuse, sequence overflow and clock rollback affect uniqueness. Introduce it only when that ordering/storage requirement justifies the extra protocol. Resetting a sequence when a clock moves backward can repeat a tuple; safe waiting or an explicitly proved alternative is required.
The main design remains numeric ranges because the agreed product does not require encoded time. Strict global ordering instead favors a shared sequencer with its latency and availability cost. Gapless invoice numbers must be tied to business commit and cancellation rules, not inferred from an ordinary allocator.
Measure range reservation latency, remaining local capacity, abandoned IDs, cursor contention, boundary exhaustion and failed safety checks. Drill lost replies, process restarts, pauses, stale backups and allocator quorum loss before optimizing steady-state throughput further.
12Check the design against its requirements
Before closing, check the final design against the agreed requirements. FR means functional requirement and NFR means non-functional requirement; the numbers refer to the lists above. These are proposed validation checks, not test results.
| Requirement | Mechanism in the final design | Validation and remaining limit |
|---|---|---|
| FR 1, 2; NFR 3 | The authority commits disjoint ranges; synchronized local cursors issue each value once. | Race concurrent range reservations and local callers, including the last value and a batch at the range boundary. |
| NFR 1, 2 | 10,000-value ranges reduce the illustrative authority rate to about 1,000 transactions/s. | Benchmark local p99 and authority throughput with bursts; stop a replenisher and measure how long existing ranges last. These estimates are not latency results. |
| NFR 3, 4 | Failover preserves allocation history; restart discards old local ranges. | Lose a range reply, restart a process and attempt stale-backup promotion. Require safe replay or refusal, never overlapping allocation. Live cursor cloning is unsupported. |
| FR 3; NFR 5 | Scoped administration and exact integer transport preserve authority and identity. | Reject unauthorized namespace changes and round-trip values above the client numeric precision limit without rounding. |
| Scope; NFR 3 | The consuming service owns its order transaction and business retry key. | Abandon an issued ID and lose an order reply; accept the gap and recover the order through its own request identity, not a new ID. |
13Rapid revision
Rehearse the numbered functional requirements and non-functional targets first. Use this table to recall the mechanisms, then close with the requirements check above.
Remember: Separate ranges between processes; synchronize within each.
| Prompt | Recall |
|---|---|
| What is the contract? | Unique integers in one namespace; gaps are allowed and issue order need not follow numeric order |
| Baseline? | Lock and advance a durable counter; return only after the required commit |
| Scaling step? | Reserve non-overlapping ranges, then issue their values locally |
| What prevents local duplicates? | Update the next-value cursor atomically or under a short lock |
| What happens after a crash? | Leave the old range unused and obtain a fresh one |
| What must failover retain? | The boundary of all allocated ranges and saved results for reservation retries |
| What handles business retries? | A separate stable request key in the service using the ID |
| Why strings on the wire? | A client’s numeric type may not represent the full integer exactly |
| When add timestamp fields? | Only when the requirement justifies managing clock errors and worker identities |
Close with: “I start with a durable counter and scale by reserving ranges. The authority proves ranges are disjoint, while each process synchronizes its cursor. Restart burns unused values instead of reconstructing uncertain local history. At the assumed load, 10,000-value ranges reduce authority traffic to about 1,000 reservations/s. The costs are gaps and no strict global issue order, both accepted in our contract.”
Practise the interview questions
Say your answer aloud before opening the model answer. Then answer the follow-up and compare the reasoning.
Which identifier properties must be clarified first?
Reveal a model answer
Clarify namespace, integer width, allowed gaps, uniqueness and ordering. This design requires distinct integers but permits gaps and non-global issue order, so durable numeric ranges suffice. Record creation, authorization and safe business retries remain separate application responsibilities.
Interviewer follow-up
Does an allocated ID imply a committed order?
Reveal the follow-up answer
No. The order transaction can still fail or be abandoned.
What the answer must demonstrate: Clarify namespace, integer width, allowed gaps, uniqueness and ordering.
How does the single-row baseline avoid duplicates?
Reveal a model answer
A short transaction locks the namespace counter, reserves its next value, advances the counter and commits before returning. Concurrent callers serialize at that row. The durability/failover policy must retain acknowledged advances; unused values may become gaps.
Interviewer follow-up
What if the reply disappears after commit?
Reveal the follow-up answer
The value remains allocated and cannot be recycled merely because its use is uncertain.
What the answer must demonstrate: A short transaction locks the namespace counter, reserves its next value, advances the counter and commits before returning.
What does reserving 10,000 IDs at a time change?
Reveal a model answer
At ten million IDs/s it reduces authority operations to roughly 1,000 range reservations/s. Each process serves calls with a local synchronized cursor until its range ends. The tradeoff is abandoned space after crashes and loss of global issue order across independent ranges.
Interviewer follow-up
What would you benchmark?
What the answer must demonstrate: At ten million IDs/s it reduces authority operations to roughly 1,000 range reservations/s.
Two threads request an ID simultaneously. Why is a range not enough?
Reveal a model answer
Disjoint ranges protect different processes, but both threads could read the same local cursor before either advances it. Use an atomic reservation or lock that checks the end and advances before returning. Batch calls obey the same boundary.
Interviewer follow-up
What happens at the exclusive end?
Reveal the follow-up answer
Switch to another committed range, wait within a deadline or fail; never issue outside the allocation.
What the answer must demonstrate: Disjoint ranges protect different processes, but both threads could read the same local cursor before either advances it.
How do you recover a generator without persisting every increment?
Reveal a model answer
Discard its entire old remainder and request a fresh disjoint range under a new incarnation. This allows gaps but avoids guessing which values were returned before the crash. A paused old process still holds a different range from its replacement.
Interviewer follow-up
What about cloning live memory?
Reveal the follow-up answer
Two clones would share a cursor and range; prevent that activation path or add an external/non-clonable allocation boundary.
What the answer must demonstrate: Discard its entire old remainder and request a fresh disjoint range under a new incarnation.
What makes allocator promotion dangerous?
Reveal a model answer
A replica missing a committed high-water advance can hand out values already reserved elsewhere. Preserve acknowledged range state and request results through the chosen promotion protocol. A stale backup needs equivalent reconciliation or refusal; restarting from its counter silently is unsafe.
Interviewer follow-up
Does an order unique constraint solve this?
Reveal the follow-up answer
It detects collisions but does not make a violated allocation contract correct.
What the answer must demonstrate: A replica missing a committed high-water advance can hand out values already reserved elsewhere.
Why may the API return decimal strings?
Reveal a model answer
Some clients cannot exactly represent all supported integers. JavaScript Number loses exactness above 2^53 − 1, so parsing through it can merge distinct IDs. A decimal string and appropriate integer type preserve the value and its namespace.
Interviewer follow-up
Can numeric sort prove creation order?
Reveal the follow-up answer
No. Different range holders progress independently; use explicit business time or a separate ordering service.
What the answer must demonstrate: Some clients cannot exactly represent all supported integers.
When would you consider a timestamp-based format?
Reveal a model answer
When compact approximate time ordering is an actual requirement. Timestamp, worker and sequence fields introduce clock rollback, worker reuse and overflow rules that numeric ranges avoid. UUID standards are another alternative when 128-bit storage is acceptable; strict global order still needs coordination.
Interviewer follow-up
Can a sequence provide gapless invoices?
Reveal the follow-up answer
Not by itself. Numbering must follow the business commit/cancellation policy.
What the answer must demonstrate: When compact approximate time ordering is an actual requirement.
Blank-page exercise · 45 minutes
Build the answer yourself
Allocate ten million integer IDs/s, then lose a range reply, restart a generator and fail over the authority.
- Agree the numbered functional requirements and non-functional targets, especially namespace uniqueness, accepted gaps/order, load and supported failures.
- Draw the durable counter baseline.
- Calculate 10,000-value range traffic.
- Prove local and cross-process disjointness.
- Explain restart, stale backup and exhaustion.
- Validate allocation, local latency, failover, exhaustion and exact transport against the numbered requirements; separate business retries and untested assumptions.
Check that each component and design decision follows from your requirements and workload.
Recall the key ideas
Answer from memory before opening each card. Explain why the choice works and what it costs. Revisit missed cards tomorrow.
Design a distributed unique-ID generatorTwo threads request an ID from A’s range at once. What prevents a duplicate?Recall first, then reveal
The authority gives processes non-overlapping ranges. Inside A, an atomic cursor update or lock gives each thread a different value. Both checks are needed.
Separate ranges between processes; synchronize within each.
Return to lessonDesign a distributed unique-ID generatorA process crashes with unused IDs in its range. May its replacement reuse them?Recall first, then reveal
No. Leave that range unused and reserve a new one, accepting gaps.
Burn uncertain values.
Return to lessonDesign a distributed unique-ID generatorWhat does a unique ID leave for the application to decide?Recall first, then reveal
How to recognize a repeated business request, record creation time, check access and provide gapless numbering if required.
A key is not the whole contract.
Return to lessonFinal revision
Summary and interview notes
One durable allocator reserves non-overlapping ranges. Each process updates its next-value cursor atomically. After a crash, leave unused values behind and reserve a new range rather than risk issuing the same ID twice.
Remember these points
- Agree the numbered functional requirements and non-functional targets before designing components; validate the final design against them.
- Then establish a durable counter.
- Batch authority work into ranges.
- Synchronize local cursor updates.
- Burn unused values after restart.
- Preserve allocation history across failover.
Interview tips
- Use explicit interval endpoints in the proof.
- Offer timestamp formats only when the requirements justify them.
Important qualifications
- Gaps and non-global issue order are intentional.
- Business idempotency remains in the consuming service.
Continue after the core interview
Explore the advanced version
The advanced lesson keeps the full detailed design. Use these sections when you want to examine the stronger requirements and failure cases.
- Custom timestamp/worker/sequence grants
Not required by the selected numeric-range contract; adds clock and worker reuse protocols.
- Clock rollback and logical-time alternatives
Relevant when choosing a time-encoded format.
- Invisible execution cloning
A stronger infrastructure failure model requires an external or non-clonable boundary.
- UUID and strict-order alternatives
Revisit format and coordination only when product properties change.
Technical references
- RFC 9562: UUIDsPrimary UUID format, uniqueness, monotonicity, clock, and overflow guidance; UUIDv7 is an alternative to the illustrative custom layout.
- ECMAScript safe integer specificationDefines the exact safe range of Number integers, motivating string transport for arbitrary 64-bit IDs.
- PostgreSQL sequence functionsConcurrent nextval behavior, gaps, and the requirement to commit before using a sequence value persistently outside the database.
Practice marks stay in this browser.