Approach 3: The Range Handler
Why this matters: this is the first design that satisfies every requirement, and the idea behind it — amortize coordination by batching it — appears everywhere once you recognize it.
Key takeaway
A central microservice (the range handler) manages ID ranges. Application servers claim a range, then assign IDs from it locally — so coordination happens once per hundred thousand IDs instead of once per ID.
How it works
The flow:
- Application servers request a range from the central handler — for example 1–1,000,000 or 1,000,001–2,000,000.
- The server stores the range start and end points locally.
- When an ID is needed, the server assigns the current value and increments it locally.
- When the range is exhausted, the server requests a new range.
Example: Server 1 claims 300,001–400,000. It assigns 300,001 to the first request, then 300,002, and so on until 400,000 is assigned. Then it queries the central handler for the next available range.
This prevents duplicates and allows concurrent responses. A load balancer distributes requests across servers to manage load.
Surviving failure
The range handler tracks range status — available versus taken — in replicated storage. A failover server ensures availability if the main handler fails, recovering state from the storage checkpoint.
Note that the handler being briefly down is not an outage: every application server is still serving IDs from the range it already holds. The system only stalls if a server exhausts its range while the handler is unavailable — which is why range size is a real tuning decision.
Pros and cons
| Detail | |
|---|---|
| Pros | Scalable, available, and guarantees unique IDs. It also satisfies the requirement for 64-bit numeric IDs |
| Cons | If an application server fails, its unassigned range is lost, creating gaps in the ID sequence. Mitigate by allocating smaller ranges — though they must be large enough to minimize network requests to the central handler |
Scorecard
| Unique | Scalable | Available | 64-bit numeric ID | |
|---|---|---|---|---|
| Using UUID | ||||
| Using database servers | ||||
| Using a range handler |
All four requirements satisfied. The solution generates unique 64-bit numeric IDs suitable for primary keys.
However, these IDs do not guarantee time-based ordering — which is where the second half of this chapter begins.
Key takeaway
Batching coordination turns a per-ID dependency into a per-range one, which is what makes the range handler both available and genuinely unique. The remaining gap is ordering: nothing about a range tells you when an ID was issued.
Interview signal by level
| Level | What a strong answer sounds like |
|---|---|
| L4 | "A service hands out ID ranges to each server." |
| L5 | Explains why it works: "each server owns a block, so it assigns locally with no coordination until the block runs out." |
| Staff+ | Generalizes and tunes it: "it amortizes coordination — once per 100k IDs instead of per ID, the same pattern as leases or connection pools. Range size trades handler load against IDs lost on server failure, and gaps are fine unless something downstream assumes contiguity. For multi-DC I'd partition the range space per DC so a partition can't cause a collision." |
Next: why unique isn't enough.