What it is
Almost every system needs an identifier it can mint without asking a single global database: a short URL code, a message id, an order id. The design question is how to get uniqueness without coordination, and which of three further properties you need: whether ids are sortable by time, whether they are small (for example a 64-bit integer), and whether they are guessable (a security question).
Important
Key takeaway: Any scheme that says “generate, check for a collision, then write” has a check-then-write race: two servers can both find no collision and both write. Eliminate collisions by construction (a counter, or time plus machine plus sequence), not by hope.
When you care
It appears inside the URL shortener, chat, orders, and feeds. The signal is dismissing the weak options with specific failure modes before choosing.
Note
Interview signal: Walking through hashing and random ids, dismissing each with a concrete concurrency failure, then landing on range-allocated counters or Snowflake-style ids is the move. Jumping straight to “I’d use a counter” without saying why the others fail misses the point of the question.
The options
| Scheme | How | Sortable by time | Size | Collisions | Needs coordination |
|---|---|---|---|---|---|
| Auto-increment (database sequence) | One counter in one database | Yes | 64-bit | None | Yes: a single bottleneck and failure point |
| Hash of the content | MD5 or SHA, truncated and encoded | No | Chosen | Yes, and a check-then-write race | No |
| Random (UUID v4) | 122 random bits | No | 128-bit | Negligible | No |
| Time-ordered random (ULID, UUID v7) | A timestamp prefix plus randomness | Yes | 128-bit | Negligible | No |
| Snowflake-style | Timestamp, machine id, sequence packed in 64 bits | Yes | 64-bit | None, by construction | A one-time machine-id assignment |
| Range allocation | A central allocator hands each server a block of counter values | Roughly | 64-bit | None | Rarely: once per block |
Hash and random short codes fail the same way. A 7-character base62 code has 62⁷ ≈ 3.5 trillion values, so a random code at 1 billion stored has about a 0.03% chance of collision per attempt. That sounds negligible, but the check for it (“is this code taken?”) then the write are two steps, and two servers doing it at once both pass the check. Hashing a long URL has the same race and, worse, a collision on content, so you must probe and retry. Both turn a uniqueness problem into a concurrency problem.
Range allocation (counter blocks)
Each application server asks a central allocator (a database row with SELECT ... FOR UPDATE, ZooKeeper, or a small service) for a
block of, say, 1,000 values, then hands them out locally with no coordination. When the block runs out it asks for another.
Encode the integer in base62 for a short code. The allocator is touched once per 1,000 ids, so one allocator supports huge write
rates, and ids are unique by construction. If a server dies, the unused remainder of its block is simply skipped. Out-of-order
ids are fine when the id only needs to be unique.
Snowflake-style ids
A 64-bit id built from three fields:
| 41 bits: milliseconds since a custom epoch | 10 bits: machine id | 12 bits: sequence |
Worked example. 41 bits of milliseconds is about 69 years from the custom epoch; 10 bits allows 1,024 machines; 12 bits allows 4,096 ids per millisecond per machine, about 4 million per second per machine. Because the timestamp is the high bits, ids sort by time, which gives chat and feeds a free ordering key and keeps database inserts mostly appending to the right edge of an index (good for B-trees).
Failure modes to name: clock moving backwards (an NTP step) can mint a duplicate or an out-of-order id, so the generator refuses to issue ids until the clock catches up or uses the last timestamp; machine-id collisions (two machines configured with the same id) cause duplicates, so machine ids come from a registry; and the sequence resets each millisecond.
Time-ordered random ids (ULID, UUID v7) put a timestamp in the high bits and randomness after it, giving sortability and no machine-id registry, at the cost of 128 bits.
Warning
Production reality: Sortable ids leak information. A timestamp prefix reveals when something was created, and sequential ids reveal volume (“order 100,482” tells a competitor how many orders you took). If ids are exposed externally, add a random component or map internal ids to opaque public ones.
When to pick what
| Situation | Choose | Why |
|---|---|---|
| Short URL code | Range-allocated counter, base62 | Unique by construction, tiny allocator load |
| Message or event ids that must order within a conversation | Snowflake-style or ULID | Time-sortable and unique |
| A single database, modest scale | Auto-increment | Simplest |
| Ids from many independent writers, no registry | UUID v4, or v7 if sortability matters | No coordination; negligible collisions |
| Public identifiers that must not be guessable | Random, or opaque mapping | No enumeration |
| Idempotency keys | Client-generated UUID | Unique per logical operation |
Caution
Common mistake: Claiming a hash or a random id is “collision-free”. It is collision-unlikely, and the check-then-write step reintroduces the race. If uniqueness is a correctness property (a short code, a primary key), enforce it with a unique constraint or build it in by construction.
Related
- Walkthrough: Designing a URL Shortener — the canonical home of range allocation: short codes encoded in base62 from counter blocks.
- Walkthrough: Designing a Chat System — where time-sortable message IDs give each conversation a free ordering key.
- Reference: Idempotency and the Outbox Pattern — client-generated UUIDs as idempotency keys, carried through every hop.