How do you choose a primary key strategy — UUID vs. auto-increment vs. a Snowflake-style distributed ID?
The 'obvious' choice — an auto-increment integer — quietly breaks the moment you shard, replicate across regions, or need to generate an ID before a row ever touches the database.
LEARN BY DOINGPut your intuition to work3 steps
How do you choose a primary key strategy — UUID vs. auto-increment vs. a Snowflake-style distributed ID?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#Three real options, three different guarantees
- Auto-increment / serial (
SERIAL,IDENTITY,AUTO_INCREMENT) — the database assigns the next integer in sequence. - UUID — a 128-bit value, almost always generated as random (v4) or, increasingly, time-ordered (v7).
- Snowflake-style ID — a 64-bit integer assembled from a timestamp, a machine/worker identifier, and a per-millisecond sequence counter, popularized by Twitter's Snowflake service and reused (with variations) by Discord, Instagram, and Sony's Sonyflake.
#Auto-increment: simple, until you need more than one writer
CREATE TABLE orders (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,customer_id BIGINT NOT NULL,total_cents INTEGER NOT NULL);
Strengths: compact (8 bytes), monotonically increasing (great for B-tree insert locality — new rows always append at the right edge of the index rather than splitting pages randomly), trivially sortable by creation order, and human-friendly in logs and URLs.
Weaknesses: it requires a single point of coordination to hand out the next value, which is exactly what you don't have once you shard across multiple database instances (two shards independently generating id=4501 is a collision waiting to merge). It's also sequentially guessable/enumerable — /orders/4502 trivially implies /orders/4501 exists, which is an information-disclosure and enumeration-attack surface unless you deliberately don't expose it. And it can't be generated by the application before an insert — you don't have an ID to reference in related records or to return to a client until the database hands one back.
#UUID: no coordination needed, at a real cost
CREATE TABLE orders (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- UUID v4, fully randomcustomer_id BIGINT NOT NULL,total_cents INTEGER NOT NULL);
Strengths: any node, anywhere — application server, mobile client, offline-first app — can generate a globally unique ID with zero coordination and zero risk of collision with any other node's IDs. Not enumerable. Ideal for merging data from multiple independently-writing sources (offline sync, multi-region active-active writes, event sourcing).
Weaknesses (this is the part interviewers actually want to hear): a random UUID (v4) has no relationship between its value and insertion order, so as rows are inserted, they land at random positions throughout the primary key's B-tree index rather than appending at the end. That means every insert has a good chance of touching a page that isn't in the buffer cache and of triggering page splits, which measurably degrades insert throughput and bloats the index compared to a sequential key — this is a well-documented, reproducible effect, not a theoretical one. It's also twice the storage of a bigint (16 bytes vs. 8), which compounds across every foreign key column referencing it and every index.
UUID v7 (and similarly, ULID) fixes the insert-locality problem by putting a millisecond timestamp in the high bits and randomness only in the low bits — so values are still globally unique and unpredictable in the low bits, but sort roughly by creation time and insert at (approximately) the right edge of the index, closing most of the gap with auto-increment while keeping the no-coordination property.
#Snowflake-style ID: the distributed-systems answer
class SnowflakeIdGenerator(private val workerId: Long, private val epoch: Long = 1_700_000_000_000L) {init {require(workerId in 0..1023) { "workerId must fit in 10 bits" }}private var lastTimestamp = -1Lprivate var sequence = 0L// Layout (64 bits): 1 unused sign bit | 41 bits timestamp | 10 bits worker id | 12 bits sequence@Synchronizedfun nextId(): Long {var timestamp = System.currentTimeMillis()if (timestamp == lastTimestamp) {sequence = (sequence + 1) and 0xFFF // 12-bit sequence, wraps at 4096/msif (sequence == 0L) {while (timestamp <= lastTimestamp) timestamp = System.currentTimeMillis()}} else {sequence = 0L}lastTimestamp = timestampreturn ((timestamp - epoch) shl 22) or (workerId shl 12) or sequence}}
Strengths: 64 bits (fits a BIGINT, half the storage of a UUID), roughly time-sortable like auto-increment (good index locality), and — the whole point — generated without a central sequence, because each worker only needs its own pre-assigned worker ID and the current time to produce an ID no other worker will produce. This is why it's the standard answer for "how do you generate IDs across many independent write nodes/shards without a bottleneck."
Weaknesses: worker ID allocation is itself a small coordination problem (usually solved once, at deploy time, via config or a coordination service like ZooKeeper/etcd, not per-ID) — and clock behavior matters: if a worker's clock jumps backwards (NTP correction), naive implementations can produce a duplicate or out-of-order ID, so production implementations detect backward clock jumps and either wait or fail loudly rather than silently emitting a bad ID.
#Decision table
| Situation | Best fit | Why |
|---|---|---|
| Single-writer monolith, no sharding planned | Auto-increment | Simplest, smallest, best index locality — no distributed problem to solve |
| IDs must be generated client-side or offline | UUID (v4 or v7) | No round-trip to the database required to get an ID |
| Merging data from multiple independently-writing systems | UUID | Zero coordination, zero collision risk regardless of source |
| Sharded/distributed writes at high volume, want compact + sortable | Snowflake-style | 64-bit, time-ordered, no central bottleneck |
| IDs will be exposed in public URLs and must not be enumerable | UUID or Snowflake with obfuscation | Auto-increment leaks sequence/volume information |
| Insert-heavy table where index bloat from random keys is a measured problem | Auto-increment or UUID v7 | Both preserve insert locality; plain UUID v4 does not |
🧠 Check your understanding
0/3 · 0/3 answered1. Why does a randomly generated UUID v4 primary key tend to hurt insert performance on a large table compared to an auto-increment integer?
2. What specific problem does a Snowflake-style ID solve that a plain auto-increment column cannot?
3. What does UUID v7 change relative to UUID v4 that helps with database performance?