Approach 3: The Range Handler
In one line: 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.
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.
The idea worth extracting: amortize the coordination
The move generalises well beyond IDs: when coordination is unavoidable, make it rare rather than making it fast.
Approach 2 needed the database once per ID. This needs the handler once per 100,000 IDs.
The coordination did not disappear — it got amortized. A server holding a range can serve at memory speed with zero network calls, and the central handler sees a request rate five orders of magnitude below the ID rate.
That pattern — batch the expensive coordinated operation and operate locally in between — recurs constantly: connection pools, token buckets, lease-based locks, and the leases from CDNs. Naming it as a general technique is stronger than describing this one instance.
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 |
Adding a data center
A common follow-up: how would you adapt range allocation when bringing a new data center online?
The clean answer is to partition the range space by data center rather than handing out globally interleaved ranges — give each DC its own high-order block, so allocations never need cross-DC coordination and a partition between DCs cannot cause a collision. Each DC then runs its own handler over its own block.
This is the same reasoning as home regions in the Foundations walkthrough: make the partition boundary match the failure boundary.
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.