Hiring BarSupport

Design a Distributed ID Generator — Staff-Level Case Study

Case study83 min read7 diagrams

Technologies referenced in this case study: PostgreSQL · ZooKeeper & etcd · Redis · DynamoDB · Cassandra · Apache Kafka

How to Use This Case Study#

Organized for interview use first, reference second. Read front-to-back once, then come back to individual sections for targeted review. Short codes for links are a different problem with different constraints — they live in URL Shortener. This case study is about the identifiers that become primary keys, public object IDs, ordering keys and shard routing keys.

ModeTimeWhat to Read
Quick Review15 minExecutive Summary → Interview Walkthrough → Fault Lines → Drills 1–3
Targeted Study1–2 hrsExecutive Summary → Walkthrough → Sections 3–4 → Deep Dives 2 and 4 → Appendix B
Deep Dive3+ hrsEverything, including the Principal Lens and all appendices
What is a Distributed ID Generator? — Why interviewers pick this topic

A distributed ID generator hands out identifiers that are unique across every machine, region and year your system will ever run — without a single database sequence on the hot path. Every row, event, message, order and upload needs one. Most of the time nobody thinks about it, which is exactly why it causes some of the quietest, most expensive incidents in production.

Before vs After — the worker-ID collision:

Without a leased worker identity:
t=0:        Autoscaler adds 40 pods; worker_id is derived from hash(hostname) % 1024
t=+0s:      Two pods hash to worker_id 617 (birthday odds with 200 pods ≈ 18%)
t=+3min:    Both generate IDs in the same millisecond with the same sequence
t=+3min:    Order table INSERT on a unique PK fails → 0.02% of checkouts 500
t=+6 days:  Events table has no unique constraint → 11,400 duplicate event IDs
t=+6 days:  Downstream dedup drops 11,400 real events as "duplicates"
t=+9 days:  Finance reconciliation finds revenue gap. Root cause: two pods, one ID space.

With leased worker IDs + fencing:
t=0:        Autoscaler adds 40 pods; each acquires worker_id via etcd lease (TTL 30s)
t=+0s:      Lease grant is atomic — no two live pods can hold 617
t=+2h:      One pod is network-partitioned; lease expires at 30s
t=+2h:      Pod's own lease watchdog stops generation at 20s (10s safety margin)
t=+2h:      Pod returns 503 for ID requests until it re-acquires; callers retry another pod
Result:     Zero duplicates. One alert: id_gen.lease_lost_total += 1.

Why interviewers reach for this question: it looks like a 10-minute bit-packing exercise, which makes it a superb level discriminator. Everyone can draw the Snowflake layout. The Staff-level signal is everything around it — what the ID is for, what happens when the clock lies, who guarantees worker identity is unique, what the ID leaks to the outside world, and which of these choices can never be undone once 10 billion rows carry them.

Mechanics Refresher: The Option Space
SchemeHow It WorksProsCons
DB auto-increment / sequenceSingle database hands out the next integerDense, tiny (8 bytes), strictly ordered, zero codeSingle writer; doesn't survive sharding; enumerable
Ticket server (Flickr-style)Dedicated DB(s) whose only job is auto_increment; two servers with odd/even offsetsSimple, dense, 64-bitA network hop per ID (or per batch); availability coupled to ticket DBs
Segment allocation (Leaf-style)Each node leases a range [max_id, max_id+step) from a DB row; serves from memory; prefetches the next range~0 hot-path latency, one DB write per step IDs, dense-ish, 64-bitGaps on restart; ordering only per node; depends on allocator DB for refills
UUIDv4122 random bitsNo coordination at all; universally supported16 bytes; random → B-tree index scatter; no time order
UUIDv7 (RFC 9562)48-bit Unix ms timestamp + 74 random/counter bitsTime-ordered, no coordination, standard format16 bytes; leaks creation time; ms-level order only
ULID48-bit ms timestamp + 80 random bits, 26-char Crockford base32Lexicographically sortable strings; monotonic modeNot a UUID type in most DBs; leaks time
Snowflake-style`timestampworker_idsequence` packed into 64 bits

For most production systems: a 64-bit Snowflake-style ID generated in-process by a shared library with leased worker IDs for primary keys, or UUIDv7 where 16 bytes are acceptable — plus a separate, opaque, prefixed public ID for anything a customer can see. The bit layout is not the interview. Worker identity, clock behavior, and what the ID leaks are.


Executive Summary

If you only read one section, read this. Everything in the case study flows from the intents and fault lines below.

What This Interview Actually Tests#

ID generation is not a bit-packing question. Everyone has seen the Snowflake diagram.

This is a uniqueness-without-coordination question that tests:

  • Whether you ask what the ID is for before choosing a format
  • Whether you know which assumption carries uniqueness (worker identity) and which carries ordering (the clock) — and what happens when each one breaks
  • Whether you treat the ID format as a one-way door that outlives the service that generated it
  • Whether you notice that an identifier is also an information channel to customers, competitors and attackers

The key insight: Uniqueness in a coordination-free generator is only ever as strong as the uniqueness of its worker identity, and ordering is only ever as good as its clock. Staff engineers spend their time on those two dependencies, not on the bit arithmetic.

The L5 → L6 → L7 Contrast — Start Here#

BehaviorSenior (L5)Staff (L6)Principal (L7)
First moveDraws the Snowflake 41/10/12 layoutAsks what the ID is for: PK, public ID, ordering key, or routing key — and splits internal from public IDsAsks how many ID schemes already exist in the org and which systems parse IDs, because the real cost is in consumers, not generators
Uniqueness"Machine ID in the bits guarantees uniqueness""Uniqueness reduces to worker-ID uniqueness, so worker IDs are leased with TTL and fencing; the DB unique constraint is the last line of defense"Makes worker-identity a platform primitive with a published contract; one lease service for all generators, audited duplicate-ID rate as an SLO
Clock"We use NTP""Clocks go backwards: small regressions are absorbed by waiting or logical time; large ones stop the generator and page; last timestamp is persisted"Sets the org clock posture: leap smearing everywhere, clock-offset SLOs on the fleet, and a rule that no correctness property may depend on cross-machine clock agreement
ExposureReturns the internal ID in the API"Internal IDs and public IDs are different columns; public IDs are opaque and prefixed; sequential IDs leak business volume"Writes the identifier standard: formats, prefixes, string encoding in JSON, what may be exposed, and the review gate for new ID types
EvolutionDoesn't discussNames the bit layout and epoch as one-way doors; leaves a version bit or reserved bitsPrices a format migration (dual-write, every consumer audited, 2–4 quarters) and refuses to embed physical shard numbers that will force one
Why "first move" separates levels

L5: Starts with the answer they memorized: "41 bits of timestamp, 10 bits of machine, 12 bits of sequence." Correct and complete for one intent — and the interviewer now has to drag the candidate back to ask whether the ID is public, whether it needs to be sortable, and whether it encodes a shard.

L6: "Before I pick a format: who sees this ID and what do we do with it? A primary key wants to be small and index-friendly. A public ID wants to be opaque and non-enumerable. An ordering key needs a monotonicity guarantee I should define precisely. A routing key embeds location. These pull in different directions, so I'll split internal and public IDs and design the internal one first."

L7: "How many ID formats are in production today? Somewhere there's a service parsing timestamps out of IDs and a mobile client storing them as doubles. The generator is a weekend of work; the consumers are the multi-year liability."

Why "uniqueness" separates levels

L5: Treats the worker-ID field as if putting it in the layout makes it unique. In production, worker IDs come from config files, hostnames or IP addresses — all of which get cloned, reused and recycled by autoscalers and container schedulers.

L6: Recognizes that the entire uniqueness guarantee has moved from the ID to the worker identity, and treats worker-ID assignment like a distributed lock: leased from a consensus store, renewed, self-fenced before expiry, and backstopped by unique constraints and a duplicate-detection metric.

L7: Notices that five teams have five generators with five ways of assigning worker IDs, and that a duplicate-ID incident in one of them is a data-corruption incident nobody will detect for weeks. Centralizes the identity lease, not the generation.

Why "exposure" separates levels

L5: Exposes /orders/1048576 and moves on. Anyone who creates two orders a day apart can compute order volume (the German tank problem, with a calculator). Anyone who edits the number can probe for authorization bugs.

L6: Separates the internal key (dense, sortable, cheap to index) from the public identifier (opaque, typed with a prefix like ord_, non-sequential). Authorization never relies on an ID being hard to guess.

L7: Turns it into a standard with a review gate: every new externally visible ID type is registered, gets a prefix, and has a documented leakage profile (time? volume? tenant?) signed off by security and legal for regulated data.

The Staff Positions#

PositionRationale
Split internal IDs from public IDsThe best primary key (dense, time-ordered) is the worst public ID (enumerable, leaks volume and time)
Generate in-process, not via a network serviceA library with a leased worker ID costs ~100ns per ID; a service hop costs 0.5–2ms and becomes a tier-0 dependency
Lease worker IDs; never derive them from hostnames or IPsUniqueness reduces entirely to worker-ID uniqueness; derivation collides silently
Time-ordered over random for primary keysRandom keys scatter B-tree inserts; at billions of rows that is 2–5× write amplification and cache misses
Never require global monotonicityCross-machine strict order needs a single sequencer; design consumers for k-sorted IDs instead
Treat the layout and epoch as a one-way doorEvery consumer that parses, sorts or stores the ID depends on it; reserve bits and version the format
64-bit IDs travel as strings in JSONJavaScript numbers lose precision above 2^53; this has bitten every public API that forgot

The Four Intents#

Four intents. They pull in incompatible directions, which is why "one ID for everything" is the most common mistake.

IntentConstraintStrategyFailure ModeCorrectness Bar
Database primary keySmall, index-friendly, cheap to generate at high rate64-bit k-sorted (Snowflake-style or segment), or UUIDv7Duplicates from worker-ID collision; index bloat from randomnessZero duplicates, enforced by unique constraint
Public / external identifierOpaque, non-enumerable, stable forever, typedRandom 96–128 bits, base62/base32, type prefix (ord_…)Enumeration, volume leakage, IDOR probingUnguessable (≥ 96 bits entropy); never reused
Ordering / sortabilityIDs sort in (approximately) creation order for cursors and time-range scansTime-prefixed IDs; define k-sorting bound explicitlyOut-of-order IDs from skew; consumers assuming strict order lose data in cursorsBounded disorder: ≤ max clock skew (e.g., 10ms)
Sharding / routing keyLocate the owning shard without a lookupEmbed a logical shard (e.g., 13 bits → 8,192 logical shards)Embedding physical topology forces an ID-format migration on reshardEvery ID routes deterministically for its whole life

🎯 Staff Move: "I'll design the internal primary key first — a 64-bit, k-sorted ID generated in-process with leased worker IDs — and treat the public ID as a separate opaque column. I'll be explicit about the ordering guarantee: per-generator monotonic, globally sorted only to within clock skew. If anyone needs strict global order, that's a sequencer, and I'd want to know why."

The Five Fault Lines#

#Fault LineThe Tension
1Coordinated vs Coordination-FreeTicket/segment allocation (dense, clock-free, depends on a DB) vs Snowflake/UUID (no hot-path dependency, depends on worker identity or randomness)
2Sortable vs OpaqueTime-ordered IDs are index-friendly and cursor-friendly — and leak creation time and volume
3Trust the Clock vs Don'tTimestamp-prefixed IDs inherit every clock pathology: skew, steps, leap seconds, VM pauses
464-bit vs 128-bit8 bytes fits bigint and halves index size but needs worker coordination; 128 bits can be random but doubles every index and breaks JSON numbers
5Meaningful vs Meaningless IDsEmbedding shard, region or type bits enables routing without lookup — and freezes topology into every stored reference

In the Wild: Real Production Systems#

Why this section belongs here: Each of these is a publicly documented design that made a specific tradeoff. Citing the tradeoff is the signal, not the name.

Twitter — Snowflake#

Twitter announced Snowflake in 2010 when it moved off MySQL auto-increment. The layout is 1 unused sign bit, 41 bits of milliseconds since a custom epoch (~69 years), 10 bits of worker identity (datacenter + worker), and 12 bits of sequence (4,096 IDs per millisecond per worker). IDs are roughly time-ordered ("k-sorted"), not strictly ordered. When the IDs exceeded JavaScript's 2^53 safe integer range, Twitter's API added string variants of the ID fields (id_str) because JavaScript clients were silently rounding them.

Staff insight: Snowflake's two most-quoted lessons are not about bits — they are that k-sorted is a weaker guarantee than people assume, and that the wire encoding of a 64-bit ID is part of the format. Both are one-way doors.

Instagram — IDs Generated Inside PostgreSQL#

Instagram's 2012 engineering post describes generating IDs in PL/pgSQL inside each logical shard: 41 bits of milliseconds since a custom epoch, 13 bits of logical shard ID (8,192 logical shards mapped onto far fewer physical Postgres servers), and 10 bits of a per-shard sequence modulo 1,024. The ID therefore tells you which logical shard owns the row, with no lookup table.

Staff insight: Instagram embedded the logical shard, not the physical server — which is what lets logical shards move between machines without changing a single ID. That distinction is the difference between a routing key and a migration you will be forced into.

Meituan Leaf — Segment Mode and Snowflake Mode#

Meituan open-sourced Leaf, which offers two modes. Segment mode keeps a row per business tag with max_id and step; each server atomically bumps max_id by step, serves IDs from memory, and loads the next segment in the background before the current one runs out (double buffering), so the database sees one write per step IDs. Snowflake mode assigns worker IDs via ZooKeeper sequential nodes, caches them locally, and checks the local clock against the time recorded by peers, refusing to start if it has gone backwards.

Staff insight: Leaf is the clearest public example of treating the two hard dependencies — allocator database availability and clock sanity — as first-class design problems with explicit mitigations, rather than footnotes.

What Interviewers Probe#

After You Say...They Will Ask...(What They're Evaluating)
"Snowflake: 41/10/12""How does each machine get its 10-bit ID? What if two get the same one?"Whether you know where uniqueness actually lives
"Timestamp in the high bits""What happens when NTP steps the clock back 2 seconds?"Clock pathology and degraded behavior
"UUIDs, no coordination""What does that do to your B-tree after a billion rows?"Storage-engine literacy, not just distributed systems
"Ticket server""It's down. What happens to writes?"Dependency thinking: prefetch, buffers, blast radius
"IDs are sortable""Strictly? Across machines? Can I use them as a cursor?"Precise ordering guarantees
"We return the ID in the API""What can a competitor learn from two of your order IDs?"Privacy and information leakage
"We'll put the shard in the ID""And when you reshard from 16 to 64?"One-way-door awareness

System Architecture Overview#

Diagram: System Architecture Overview

Reading the diagram: The hot path is entirely in-process: no network call per ID. The control plane (etcd leases, format registry) is touched at boot and every ~10s for lease renewal — if it's unavailable, generators keep working until their lease's safety margin runs out. The segment allocator is an alternative for consumers that need dense IDs. The database's unique constraint and the db.duplicate_key_errors metric are the backstop that detects a uniqueness failure the generator didn't prevent.

Quick-Reference: The 30-Second Cheat Sheet#

TopicThe L5 AnswerThe L6 Answer — Say ThisThe L7 Answer — Say This
Format"Snowflake, 41/10/12""64-bit k-sorted for PKs; separate opaque prefixed public ID; UUIDv7 if 16 bytes is fine""One registry of ID formats, versioned. New formats need a review because consumers parse them for a decade."
Worker ID"From config""Leased from etcd with TTL; generator self-fences before expiry; unique constraint as backstop""Identity leasing is a platform primitive shared by every generator, lock and scheduler in the org"
Clock"NTP""Small regressions: wait or borrow from logical time. Large: stop and page. Persist last timestamp.""Fleet-wide leap smear, clock-offset SLO, and no correctness property depends on cross-host clock agreement"
Ordering"IDs are sorted""Per-generator monotonic; globally k-sorted within skew. Cursors use (time, id) with a skew overlap.""Strict global order is a sequencer and a cost line; I'd make teams justify it"
Privacy"UUIDs are random enough""Sequential IDs leak volume; time-prefixed IDs leak creation time. Public IDs are opaque; authz never depends on secrecy.""Leakage profile documented per ID type and signed off by security"
Evolution—"Reserve a version bit; epoch and layout are one-way doors""A format migration is 2–4 quarters of consumer audits — budget it or avoid it"

Key Numbers Worth Memorizing#

MetricValueWhy It Matters
41 bits of milliseconds~69.7 years from epochPick a recent custom epoch; Unix epoch wastes 56 years of range
12-bit sequence4,096 IDs per ms per worker (~4.1M/s)Ceiling per generator before it must wait for the next ms
10-bit worker ID1,024 concurrent generatorsAutoscaled fleets plus restarts can exhaust this; leases must expire
Snowflake cluster ceiling~4.2B IDs/s (1,024 × 4.1M)Never the bottleneck — worker-ID space is
JavaScript safe integer2^53 − 1 ≈ 9.0 × 10^15With a 22-bit shift, IDs pass 2^53 about 25 days (2^31 ms) after the epoch — always encode as string
UUIDv4 random bits12250% collision at ~2.7 × 10^18 IDs; collisions are never the real risk
UUIDv7 random bits74 (12 + 62)Collision only within the same ms; still astronomically safe
In-process generation~50–200ns per IDvs 0.5–2ms for a network ID service
Segment step sizingstep ≈ 10–60s of peak rate100K IDs/s with step 1M → one allocator write every 10s
Random-key B-tree page fill~50–70% vs ~90%+ sequentialRandom PKs grow index size and buffer-pool misses
Typical intra-DC NTP offsetsub-ms to a few msSets your k-sorting bound; across regions, tens of ms
Index cost of 16-byte vs 8-byte PK+8 bytes × rows × (1 + secondary indexes) in clustered engines1B rows × 5 indexes ≈ 48 GB extra

Interview Walkthrough

The most common mistake: Candidates spend 20 minutes on bit arithmetic — "41 bits gives 69 years, 12 bits gives 4,096 per ms" — and never reach worker-ID leasing, clock regression, or public-ID leakage. The arithmetic takes 90 seconds. Spend the rest on the assumptions it rests on.


Phase 1: Requirements & Framing (2–3 minutes)#

State the functional requirement in one sentence:

"Generate identifiers that are unique across all machines and all time, at the rate our write path needs, without a single database sequence on the hot path."

Then spend the time on intent and the non-functional bar:

"Before I pick a format, four questions. Who sees the ID — only our databases, or customers too? Do we need IDs to sort by creation time, and how strictly? Does the ID need to tell us which shard owns the row? And what's the peak rate — thousands per second, or millions? I'll assume internal primary keys for a sharded OLTP store, peak ~500K IDs/s across ~300 service instances, rough time ordering for cursor pagination, and a separate public identifier for anything customer-facing."

Commit to the constraint set:

"So: 64-bit to stay in bigint and keep indexes small; generated in-process, no network hop; zero duplicates — enforced, not hoped for; k-sorted within clock skew, which I'll quantify; and the public ID is opaque."

🎯 Staff Move: Naming "public ID is a separate column" in the first two minutes heads off the enumeration probe and signals you've seen the incident. It costs one sentence.


Phase 2: Core Entities & API (1–2 minutes)#

  • IdFormat: layout_version, epoch_ms, bit widths (ts, worker, seq), encoding (decimal string / base32)
  • WorkerLease: worker_id, holder (pod UID), lease_id, expires_at, fencing_token
  • Segment (only if dense IDs are needed): biz_tag, max_id, step, updated_at

The generator is a library, not a service:

IdGenerator.next() → int64                // hot path, ~100ns, in-process
IdGenerator.nextBatch(n) → int64[n]       // for bulk inserts
IdGenerator.parse(id) → {ts_ms, worker, seq, version}   // debugging only — not a contract
PublicId.mint(type) → "ord_7Fq2Kx9mW3pLzR8vN"            // 128 random bits, base62, prefixed

For callers that cannot embed the library (legacy stacks, SQL jobs):

POST /v1/ids:batch   { "count": 1000 }   →   { "ids": ["732...", ...], "layout_version": 1 }

🎯 Staff Move: "parse() exists for debugging, and I'd say in the API docs that the ID's internal structure is not a contract. The day a team starts using the embedded timestamp as their created_at is the day the layout becomes impossible to change."


Phase 3: High-Level Architecture (≤5 minutes)#

Diagram: Phase 3: High-Level Architecture (≤5 minutes)

Walk it in 60 seconds:

  1. On boot, the instance acquires a worker ID from etcd as a lease (worker/617 → pod-uid, TTL 30s). No lease, no IDs.
  2. Each next() reads the clock, compares it to the last timestamp, increments the sequence or resets it, and packs the bits. No network.
  3. The watchdog renews the lease every 10s. If renewal fails for 20s, the generator stops issuing IDs before the lease can be granted to someone else.
  4. The database's unique constraint is the last line of defense, and its duplicate-key error rate is a monitored metric.
  5. The public ID is minted independently with 128 bits of randomness and stored in its own uniquely indexed column.

🎯 Staff Move: "That's the design that works. Now the interesting part is the three assumptions it rests on: worker IDs are unique, the clock moves forward, and nobody depends on the ID's internals. Let me go through how each one fails."


Phase 4: Transition to Depth (1 minute)#

"The bits are the easy part. Three places I'd go deep: how worker identity is guaranteed unique under autoscaling and partitions, what the generator does when the clock goes backwards, and what the ID format commits us to for the next ten years — including what it leaks publicly. Which matters most for your system?"

If the interviewer has no preference, lead with worker identity — it is where silent duplicate IDs come from, and silent corruption outranks loud outages.


Phase 5: Deep Dives (25–30 minutes)#

Deep dive A — Worker identity (8–10 min). Walk the options: static config (duplicates on copy-paste), hash of hostname/IP (birthday collisions: 200 pods into 1,024 slots ≈ 18%), Kubernetes StatefulSet ordinal (unique per StatefulSet, not across clusters), consensus-store lease (the default). Explain the lease-expiry race and the self-fencing margin. Say the sentence: "A generator that cannot prove it still holds its worker ID must stop generating."

Deep dive B — Clock behavior (6–8 min). Clocks go backwards on NTP step corrections, VM live migration, leap seconds without smearing, and operator error. Policy: regression ≤ 5ms → spin until the clock catches up; regression > 5ms → either continue on a logical clock (max(now, last) with sequence overflow advancing logical time) or stop and page. Persist the last-issued timestamp so a restart after a backwards step doesn't reissue.

Deep dive C — Format as a one-way door (5–7 min). Epoch choice, bit budget, version bit, JSON string encoding, what consumers will do with the timestamp. Why logical shard bits are fine and physical shard bits are not.

Deep dive D — Public IDs and leakage (4–5 min). Enumeration, German-tank volume estimation, creation-time leakage, IDOR probing. Opaque prefixed IDs; authorization never relies on unguessability.

Deep dive E — If the interviewer wants dense IDs (4–5 min). Segment allocation with double buffering: DB write rate = ID rate / step, prefetch at 10–20% consumed, gaps on crash are acceptable, allocator DB outage tolerance = buffered IDs / consumption rate.


Phase 6: Wrap-Up (2–3 minutes)#

"To summarize: 64-bit k-sorted IDs generated in-process; uniqueness rests on leased worker IDs with self-fencing and a unique-constraint backstop; ordering rests on the clock, so we absorb small regressions and stop on large ones; public IDs are a separate opaque column. Day one, I'd ship the library and the lease service. I'd defer a network ID service, dense segment IDs, and multi-region bits until a consumer actually needs them — but I'd reserve two bits now so adding region or format version later doesn't require a migration."

🎯 Staff Move: Close with what you deferred and the bits you reserved. It shows you priced the one-way door.


Common Timing Mistakes#

MistakeTime LostFix
Deriving 69 years from 41 bits on the whiteboard3–5 minState the number; move on
Comparing six ID formats before choosing one8–10 minAsk the intent, pick one, mention one alternative
Building an "ID service" with load balancers and replicas5–8 minIn-process library; a service only for stacks that can't embed it
Never mentioning public IDs— (fails probe)One sentence in Phase 1
Treating clock regression as "rare, ignore"— (fails probe)Have a policy with a threshold and an owner

1. The Staff Lens#

1.1 Why This Problem Exists in Staff Interviews#

ID generation is the smallest problem that contains every Staff-level theme. There is a single correctness property (uniqueness) that rests on an assumption the system does not control (worker identity). There is an ordering property that rests on another uncontrolled assumption (the clock). There is a storage-engine consequence (index locality) that a distributed-systems-only candidate misses. There is a security and privacy surface (what the ID leaks). And the format is a one-way door that will outlive every engineer in the room. A candidate who can find all five in a "simple" question will find them in a hard one.

1.2 The L5 → L6 → L7 Contrast — Visual#

Diagram: 1.2 The L5 → L6 → L7 Contrast — Visual

The L5 path is a correct implementation of one intent. The L6 path interrogates the assumptions. The L7 path notices that the generator is cheap and the consumers are expensive, and governs the consumers.

1.3 The Staff Question That Cuts Through Everything#

"If two machines believe they are the same worker for ten seconds, what happens?"

Every design answers this question, explicitly or not. Snowflake without leases: duplicate IDs, detected (maybe) by a unique constraint. Snowflake with leases and self-fencing: impossible, at the cost of a generator that stops when it loses contact with the lease store. Segment allocation: impossible, because each range is handed out by an atomic DB update. UUIDv4/v7: irrelevant, because there is no worker identity — uniqueness rests on 74–122 bits of randomness instead. Ask this question and the design space sorts itself.


2. Problem Framing & Intent#

2.1 The Four Intents — Explained#

Intent 1: Database primary key. The ID is written into a clustered index and copied into every secondary index (InnoDB) or referenced by every foreign key. Size and insertion locality dominate (see Database Indexing). An 8-byte, time-ordered key appends to the right edge of the B-tree: pages fill to ~90%+, the hot pages stay in the buffer pool, and insert cost stays flat as the table grows. A 16-byte random key inserts into a random leaf: page splits leave pages 50–70% full, the working set becomes the entire index, and once the index exceeds memory every insert is a random read. The correctness bar is zero duplicates — but the enforcement is the unique constraint, and the generator's job is to make that constraint never fire.

Intent 2: Public / external identifier. The ID appears in URLs, receipts, support tickets, webhooks and customer databases. It must be stable forever (customers store it), opaque (it should reveal nothing), non-enumerable (adjacent IDs shouldn't exist), and ideally typed — ord_, cus_, inv_ prefixes make support tickets and logs self-describing and catch "passed a customer ID where an order ID was expected" bugs at the API boundary. The correctness bar is ≥ 96 bits of entropy and no reuse, ever.

Intent 3: Ordering / sortability. Feeds, timelines, audit logs and cursor pagination want IDs that sort by creation time, so WHERE id > :cursor ORDER BY id LIMIT 50 replaces a secondary index on created_at. The trap is assuming strictness. Snowflake IDs from two workers are ordered only to within the clock offset between them — an ID minted at 12:00:00.003 on worker A can be smaller than one minted at 12:00:00.001 on worker B if A's clock is 5ms behind. A cursor that reads "everything after ID X" can permanently skip rows that commit late with smaller IDs. Staff answer: define the guarantee (per-worker monotonic, global disorder ≤ ε) and design consumers to tolerate ε.

Intent 4: Sharding / routing key. Embedding a shard identifier lets any service route a read by inspecting the ID — no directory lookup, no extra hop. Instagram's 13 logical-shard bits are the canonical example. The trap is embedding physical topology: if the ID says "server 7," resharding means rewriting every ID or maintaining a translation table forever. Embed a logical shard from a large fixed space (4,096–8,192), map logical to physical in a small directory, and the mapping can change without touching any ID. Sharding & Partitioning and Database Sharding cover the directory side.

Committing:

🎯 Staff Move: "These four want different things: PKs want dense and ordered, public IDs want random and opaque, ordering wants a time prefix, and routing wants embedded location. I'm not going to find one ID that does all four well. I'll make the internal ID serve PK + ordering + logical routing, and mint a separate public ID. That's two columns and it removes an entire class of incidents."

2.2 When NOT to Build a Distributed ID Generator#

SituationUse InsteadWhy
Single primary database, < ~5K writes/s, no plan to shard within 2 yearsDB sequence / bigserialDense, ordered, free; sequences in PostgreSQL are non-transactional and cache-able, so they rarely bottleneck
You need 16 bytes anyway (e.g., federated data, offline clients)UUIDv7 generated by the client or DBNo worker identity to manage; time-ordered
Offline-first clients creating records before syncUUIDv4/v7 on deviceDevices cannot hold worker leases
Short human-facing codesSee URL ShortenerDifferent constraints: length, typability, enumeration
Strict global order for a ledger or logA single sequencer per partition (e.g., Kafka partition offsets) or the DB's commit sequenceCoordination-free generators cannot give strict order
Idempotency keysClient-generated UUIDv4Must exist before the first network call; see Idempotency

🎯 Staff Move: "If we have one Postgres primary doing 2K inserts a second, I wouldn't build anything — bigserial is the right answer and I'd revisit when we shard. The question gets interesting when a sequence becomes a cross-shard coordination point."

2.3 What the Interviewer Leaves Underspecified#

UnderspecifiedWhy It MattersWhat to Assume Out Loud
Who sees the IDPublic exposure changes format, entropy and leakage requirements"Internal PK; separate public ID"
Ordering strictness"Sortable" ranges from "roughly by day" to "strict total order""k-sorted within clock skew; per-worker monotonic"
Peak rate and fleet sizeDetermines sequence bits and worker-ID bits"500K/s peak, ≤ 1,024 concurrent generators"
ID lifetimeDetermines epoch and timestamp bits"Must not overflow for 50+ years"
Languages and clientsJSON number precision, DB types"64-bit, string in JSON"
Multi-regionRegion bits, lease-store placement"Single region now; reserve 2 bits for region"
Whether IDs may have gapsSegment and Snowflake both produce gaps"Gaps are fine; nothing counts by subtracting IDs"

2.4 Precise Terminology#

TermMeaningCommon Confusion
UniqueNo two entities ever receive the same IDOften conflated with "unguessable"
Monotonic (per generator)Each ID from one generator is greater than the lastNot the same as globally ordered
k-sortedIDs are sorted to within a bounded disorder (k ms, or k positions)Mistaken for strict order; cursors break
Strictly orderedID order equals real-time creation order system-wideRequires a single sequencer or synchronized clocks with bounded uncertainty (e.g., Spanner TrueTime commit-wait)
DenseNo gaps between consecutive IDsAlmost never needed; never promise it
OpaqueReveals nothing about time, volume, locationTime-prefixed IDs (UUIDv7, ULID, Snowflake) are not opaque
Worker IDThe generator identity embedded in the IDIt is the uniqueness guarantee, not decoration
EpochThe zero point of the timestamp fieldA one-way door; changing it re-orders all IDs
Fencing tokenMonotonic number proving lease currencyWithout it, an expired lease holder can still act

3. The Five Fault Lines#

3.1 Fault Line 1: Coordinated vs Coordination-Free#

The tension: Ticket servers and segment allocators get uniqueness from a database's atomic increment — no clock, no worker identity, dense IDs. They also make the database a dependency of every write. Snowflake and UUIDs remove the per-ID dependency but move the uniqueness guarantee to worker identity or to randomness.

OptionPer-ID CoordinationUniqueness Rests OnOrderingWho Pays
Ticket server (per ID)1 DB round trip (0.5–2ms)DB auto-incrementStrict-ish (per server)Every writer (latency); DB on-call (tier-0 dependency)
Segment allocation1 DB write per step IDsAtomic UPDATE max_id = max_id + stepPer node only; ranges interleaveAllocator owner (refill outage = write outage after buffer drains)
Snowflake + leased workerNone per ID; lease renew every ~10sLease store's mutual exclusion + clockk-sortedPlatform (lease service); on-call (clock pages)
UUIDv7 / ULIDNone74–80 random bitsk-sorted at msStorage (16 bytes per key, ×N indexes)
UUIDv4None122 random bitsNoneStorage (random index inserts)

Staff default: Snowflake-style with leased worker IDs for 64-bit PKs. Segment allocation when a consumer genuinely needs dense-ish integers (e.g., an external system with a 32-bit field, or a regulatory "invoice numbers must be sequential" requirement — and even then, per-tenant). UUIDv7 when 16 bytes is acceptable and you want zero control-plane dependency.

When to deviate: If the platform has no reliable consensus store, segment allocation is safer than Snowflake with hand-assigned worker IDs, because its uniqueness comes from a single atomic DB update you already know how to run.

🎯 Staff Move: "I'll pick coordination per-lease rather than per-ID. Snowflake moves the coordination from every write to one lease renewal every ten seconds — that's a 10^6 reduction in coordination traffic, and the lease store can be down for 20 seconds before anyone notices."

3.2 Fault Line 2: Sortable vs Opaque#

The tension: Time-ordered IDs make B-trees happy and cursors trivial. They also tell the world when each object was created, and — if sequential — how many objects you create.

What LeaksFromWho Exploits ItWho Pays
Business volumeDense or per-worker sequential IDsCompetitors, analysts (two order IDs a week apart = weekly volume)The business
Creation timeAny time-prefixed IDAnyone correlating account age, deanonymizing activityUsers (privacy)
Existence of other objectsEnumerable IDsScrapers; IDOR attackers probing /invoices/{id+1}Security; customers whose data leaks
Fleet topologyWorker/datacenter bitsAttackers mapping infrastructureSecurity (minor)

Staff default: Internal IDs are time-ordered and never leave the trust boundary. Public IDs are opaque: 128 random bits base62-encoded with a type prefix, or the internal ID encrypted with a keyed permutation if you must avoid a second index (see Appendix B). Authorization never depends on an ID being unguessable — unguessability is defense in depth, not access control.

When to deviate: Some public IDs are meant to be sortable (tweet IDs, chat message IDs used as cursors by clients). That's fine if the time leakage is an accepted product decision with a named owner — Discord publicly documents that its snowflakes encode a timestamp.

3.3 Fault Line 3: Trust the Clock vs Don't#

The tension: A timestamp prefix gives ordering and makes worker-ID reuse safe across time. It also imports every clock failure into your uniqueness guarantee: if the clock goes back 3 seconds and the generator trusts it, it will reissue every (timestamp, sequence) pair from those 3 seconds.

How clocks go backwards in practice:

CauseTypical MagnitudeFrequency
NTP slew (normal correction)microseconds per secondConstant — invisible, monotonic
NTP step (large offset correction, e.g., after boot)100ms – secondsRare per host; frequent across a 10K-host fleet
VM live migration / pause10ms – secondsCloud-dependent
Leap second without smearing1s repeatHistorically every 1–3 years (none since 2016; CGPM voted in 2022 to phase out by 2035)
Operator sets the clockAnythingThe one that causes the worst incident

Options:

PolicyBehaviorWho Pays
Trust itReissue timestamps → duplicatesData integrity — silent
Spin-waitBlock until now > lastCallers (latency = regression size)
Logical clockUse max(now, last); when the sequence overflows, advance logical ms by 1Ordering accuracy (IDs run "in the future" until wall clock catches up)
RefuseStop issuing IDs; return error; pageAvailability of that one instance

Staff default: Regression ≤ 5ms → spin. 5ms–1s → continue on a logical clock, increment id_gen.clock_regression_total, and alert if it persists > 60s. > 1s → stop issuing, mark the instance unhealthy so the load balancer drains it, and page. Persist the last-issued timestamp (to local disk every second, or to the lease record) so a process restarted after a backwards step resumes from max(now, persisted_last + safety_window).

When to deviate: If you use UUIDv7, the random bits make same-ms reissue harmless for uniqueness — the clock only affects ordering, so "trust it" becomes acceptable. That's a major reason to prefer UUIDv7 when 16 bytes is affordable.

🎯 Staff Move: "The clock policy needs an owner and a threshold, not a shrug. I'd rather have one instance refuse IDs and get drained than have it silently mint duplicates for three seconds — drained instances are a capacity blip, duplicates are a data-corruption incident."

3.4 Fault Line 4: 64-bit vs 128-bit#

The tension: 64 bits fits a native integer type in every database and language, halves index size, and keeps joins cheap — but 64 bits isn't enough room for randomness, so uniqueness needs worker coordination. 128 bits can be mostly random and coordination-free — at double the key size everywhere the key is copied.

Dimension64-bit (Snowflake / segment)128-bit (UUIDv7 / ULID)
Storage per key8 bytes16 bytes (36 as text — never store as text)
CoordinationWorker leases or allocator DBNone
JSONMust be a string (2^53 limit)Already a string
Clock failure impactDuplicates (unless guarded)Ordering only
Native DB typebigint everywhereuuid in Postgres; BINARY(16) in MySQL
Who paysPlatform (lease service, clock guards)Storage / DBAs (bigger indexes, more memory)

Staff default: 64-bit for high-volume core tables where index size drives cost (events, messages, ledger lines). UUIDv7 for everything else — it removes an entire control plane. The decision is per-table, and that's fine.

3.5 Fault Line 5: Meaningful vs Meaningless IDs#

The tension: Every bit of meaning in an ID — shard, region, type, tenant — saves a lookup and costs flexibility. Once 10 billion stored references carry a meaning, that meaning cannot change.

Embedded MeaningBenefitBreaks WhenWho Pays
Physical shardRoute without lookupReshardData platform (multi-quarter migration)
Logical shard (4K–8K space)Route via tiny directoryLogical space too smallRarely anyone — the right compromise
RegionRoute to home regionData residency change, region mergeCompliance / infra
TenantIsolation, per-tenant scansTenant merges, acquisitionsProduct (customer migrations)
Entity typeSelf-describingNever, if done as a public prefixNobody — do it in the public ID string, not the bits

Staff default: Embed only a logical shard (if routing matters) and a format version. Put type in the public ID prefix. Put region and tenant in columns. "Meaning in the ID is a cache of a lookup that can never be invalidated."

🎯 Staff Move: "I'll reserve 1 version bit and 2 region bits now and leave them zero. Reserved bits are cheap; a format migration costs quarters."


4. Failure Modes & Operational Reality#

ID generators fail in two shapes: loud (no IDs → writes fail → someone gets paged in 60 seconds) and silent (duplicate or misordered IDs → data quietly corrupts → someone finds it in a reconciliation three weeks later). A Staff design converts silent failures into loud ones, deliberately.

4.1 Duplicate Worker IDs — The Silent One#

t=0:        Deploy changes worker_id source from etcd lease to env var WORKER_ID
            (to "remove a dependency"); Helm chart templates it from the pod ordinal
t=0:        Two StatefulSets in two clusters both have ordinal 0..63 → 64 colliding pairs
t=+1min:    Collision requires same ms AND same sequence on both twins
            At ~2K IDs/s per pod, P(same ms, same seq) per ms ≈ low but nonzero
t=+1h:      orders table: 3 duplicate-key errors (unique PK) → 3 failed checkouts, retried OK
t=+1h:      events table: no unique constraint (append-only, "for speed") → duplicates land
t=+12 days: Analytics pipeline dedups by event_id → drops ~0.004% of real events
t=+19 days: Billing usage underreported by 0.004% across 2,100 customers. Found by finance.

Detection: db.duplicate_key_errors{table} > 0 is a page, not a warning — on a correct generator it is exactly zero. A sampled background job that checks (worker_id, ts) uniqueness across generators (id_gen.worker_lease_conflicts), and a startup check: every generator logs (worker_id, lease_id, holder) and an auditor alerts if two holders report the same worker ID within a TTL.

Blast radius: Every table written by the colliding pair. Tables without unique constraints absorb duplicates silently; downstream dedup turns duplicates into data loss.

Mitigation: Stop the affected generators (fail closed), re-lease, then run the duplicate audit on tables without unique constraints for the window since the change.

Prevention: Worker IDs only from leases; the env-var path does not exist in the library. Unique constraints on every table whose key is generated, including append-only ones — the 3–5% write cost is the price of detection. Contract test in CI: spin up two generators with the same config and assert the second refuses to start.

Owner: The ID platform team owns the library and lease service; each data-owning team owns its unique constraints. The post-mortem action item belongs to whoever approved removing the lease dependency.

4.2 Clock Goes Backwards#

t=0:        Cloud host performs live migration of 120 VMs; guest clocks pause ~1.8s
t=+2s:      chrony detects 1.8s offset; configured with makestep 1.0 -1 → steps clock back
t=+2s:      Generators on 120 instances see now < last by ~1,800ms
            Policy (old): spin-wait until now > last → each next() blocks up to 1.8s
t=+2s:      Request threads pile up; p99 write latency 12ms → 1,900ms
t=+5s:      Thread pools exhausted on 120 instances; health checks time out
t=+20s:     Load balancer ejects 120 instances (40% of fleet) at once; remaining 60% overload
t=+3min:    Partial outage; recovered as clocks pass "last" and instances re-admit

Detection: id_gen.clock_regression_ms (histogram), id_gen.wait_time_ms p99, host-level ntp.offset_ms and ntp.step_events_total.

Blast radius: Correlated — clock events are fleet-wide (same hypervisor, same NTP source, same leap second). A per-instance policy that is fine for one host becomes an outage across 40% of hosts.

Mitigation: Policy tiers: spin only up to 5ms; logical-clock continuation up to 1s; above 1s, refuse with a fast error (not a blocking wait) so the instance is drained cleanly rather than hanging. Configure NTP to slew, not step, after boot (makestep only in the first few updates), and smear leap seconds.

Prevention: Game day: inject a 2s backwards step on 10% of a canary fleet and verify drain-not-hang. A regression > 1s is an infrastructure incident — page the compute/time team, not just the service.

Owner: Time infrastructure (NTP/chrony config, leap smear) is owned by the compute platform. The generator's regression policy is owned by the ID library team. The post-mortem needs both in the room.

4.3 Allocator / Lease Store Unavailable#

Segment mode, step = 10,000, peak 50K IDs/s per biz_tag across 20 nodes:
t=0:        Allocator MySQL primary fails; failover takes 45s
t=0:        Each node holds current segment + prefetched next (double buffer)
            Per node: ~2.5K IDs/s; buffer ≈ up to 20,000 IDs ≈ 8s of headroom
t=+8s:      Nodes exhaust buffers → next() errors → order writes fail
t=+45s:     Failover completes; refills resume; 37s of write outage

Detection: id_gen.segment_remaining per node (alert when < 2× refill latency worth), id_gen.refill_latency_ms, id_gen.refill_errors_total.

Blast radius: Every writer of that biz_tag — usually an entire product line.

Mitigation & prevention: Size the buffer to survive the allocator's worst failover: step ≥ peak_rate_per_node × (failover_time × 2). Here: 2.5K/s × 90s ≈ 225K per segment. Dynamic step (Leaf adjusts step based on how quickly the last segment was consumed). Gaps on restart grow with step — fine, because nobody depends on density.

For Snowflake with leases, the equivalent is lease-store unavailability: with TTL 30s and a 10s safety margin, generators keep working for ~20s after etcd becomes unreachable, then stop. Mitigation is identical: TTL ≥ 2× the lease store's realistic failover time (etcd leader election is typically 1–3s, so 30s is generous) — and a new instance can't start without a lease, so a long lease-store outage blocks scale-out, not steady state.

Owner: The ID platform team owns allocator/lease-store availability and the buffer sizing formula. Product teams own their write-path behavior when next() fails (retry another instance, queue, or fail the request).

4.4 Sequence Exhaustion Under Burst#

t=0:        Bulk import job calls next() in a tight loop on one instance
t=0:        12-bit sequence → 4,096 IDs per ms → generator waits for next ms
t=0:        Import runs at 4.1M IDs/s — fine for the import
t=0:        Same instance also serves user writes; their next() calls queue behind the loop
t=+1s:      User-facing p99 for writes on that instance 8ms → 140ms

Detection: id_gen.seq_exhausted_total (counts ms rollovers due to exhaustion), lock-contention time on the generator mutex.

Mitigation: Bulk paths use nextBatch(n) on dedicated instances or a dedicated worker ID; generator uses a lock-free CAS loop on a packed (ts, seq) word so waiters don't convoy. If one logical stream legitimately needs > 4M IDs/s, give it multiple worker IDs — don't widen the sequence (that's a format change).

Owner: Calling team for workload isolation; ID library for lock-free implementation.

4.5 The Format Overflows Its Consumers#

Not a generator failure — a consumer failure, and the most common one in public APIs.

Consumer BugSymptomPrevention
JavaScript JSON.parse of 64-bit numeric IDIDs silently rounded; wrong object fetched or 404Always serialize as string; lint API schemas for int64 in JSON
Spreadsheet / CSV exportExcel shows 7.32E+17, last digits zeroedString format with a non-numeric prefix in exports
Signed 32-bit column in a partner systemTruncation, collisionsPublish the type contract; reject at integration review
Code that extracts timestamp from ID as created_atBreaks on layout change or epoch changeparse() documented as debug-only; real created_at column
Sorting IDs as strings"10" < "9"Fixed-width encoding (zero-pad decimal, or base32 like ULID)

Owner: API governance (schema lint), ID platform (format docs), each consuming team.

4.6 Enumeration and Leakage Incident#

t=0:        Public invoice URL /invoices/1840211 (internal Snowflake-less sequence)
t=+1d:      Researcher increments ID, finds 3% of invoices load without auth (IDOR bug)
t=+1d:      Separately, analyst estimates monthly invoice volume from two IDs 30 days apart
t=+2d:      Disclosure; security incident; press asks about revenue numbers

Detection: api.404_rate_by_client spikes on sequential probing; WAF rule on monotonic ID scans per client.

Mitigation: Fix the authorization bug (the real vulnerability). Introduce opaque public IDs; keep the old URLs working behind auth with redirect-to-new-ID.

Owner: Security owns the IDOR fix; the product team owns migrating to public IDs; API governance owns the rule that blocks new endpoints from exposing internal keys.

4.7 Operational Reality Matrix#

FailureDetection SignalBlast RadiusMitigationOwner
Duplicate worker IDsdb.duplicate_key_errors > 0; id_gen.worker_lease_conflictsAll tables written by colliding pair; silent where no unique constraintStop generators, re-lease, audit windowID platform + data owners
Clock regression (small)id_gen.clock_regression_ms p99 < 5msLatency on affected instanceSpin / logical clockID library
Clock regression (large, fleet)ntp.step_events_total spike; id_gen.refusals_totalCorrelated: many instancesRefuse fast, drain, fix NTPCompute platform (time)
Lease store downid_gen.lease_renew_errors; etcd.leader_changesNew instances can't start; existing stop after marginTTL ≥ 2× failover; alert at first errorID platform
Allocator DB downid_gen.segment_remaining lowAll writers of a biz_tag after buffer drainsBigger buffers, dynamic stepID platform
Sequence exhaustionid_gen.seq_exhausted_totalOne instance's write latencyBatch API, separate worker IDsCalling team
JSON precision lossClient error reports; 404s on valid IDsEvery JS clientString encodingAPI governance
EnumerationSequential 404 scansCustomer data / business metricsOpaque public IDs + authzSecurity + product
Timestamp bit overflowCalendar (decades out)EverythingEpoch choice, version bitID platform (registry)

5. Evaluation Rubric#

5.1 Level-Based Signals#

DimensionSenior (L5)Staff (L6)Principal (L7)
Problem framingTreats it as "generate unique numbers fast"Separates PK, public, ordering, routing intents; splits internal/public IDsInventories existing ID schemes and consumers; frames the problem as governance of a format
UniquenessBits in layoutLeased worker IDs, self-fencing, unique-constraint backstopShared identity-lease primitive; duplicate-ID rate as an org SLO
Clock"NTP is accurate"Regression policy with thresholds; persisted last timestampFleet time posture (smear, slew-only, offset SLO); correlated-failure awareness
StorageDoesn't connect ID to indexExplains B-tree locality, page fill, key size × secondary indexesPrices it: GB of index, buffer-pool $, write amplification at fleet scale
Ordering"Sorted"Per-worker monotonic, k-sorted bound, cursor overlapPushes back on strict-order requirements; offers sequencer as a priced alternative
PrivacyNot mentionedEnumeration, volume, time leakage; opaque prefixed public IDsLeakage profile per ID type, signed off by security/legal
EvolutionNot mentionedEpoch and layout as one-way doors; reserved bitsMigration cost model; registry; deprecation plan for legacy schemes

5.2 Strong Hire Signals#

SignalWhat It Sounds Like
Locates the real guarantee"Uniqueness here is exactly as strong as worker-ID uniqueness. So the worker ID is a lease, not a config value."
Converts silent to loud"I'd rather a generator refuse IDs than mint a duplicate; refusal is a capacity blip, duplicates are corruption."
Storage literacy"Random 16-byte keys in a clustered index mean every insert hits a random page. At a billion rows that's the whole index in the working set."
Precise ordering"Globally k-sorted within clock skew — about 10ms in one region. Cursors re-read a 10ms overlap and dedupe."
Leakage awareness"Two order IDs a week apart tell a competitor our weekly order volume. Public IDs are opaque."
One-way doors"Epoch, layout and JSON encoding outlive us. I'd reserve a version bit today."

5.3 Lean No-Hire Signals#

SignalWhy It Misses the Bar
Builds a replicated network "ID service" as the first answerAdds a tier-0 dependency and 1ms per write to solve a problem a library solves in 100ns
Worker ID from hostname hash or IP octetSilent collisions under autoscaling; shows no model of where uniqueness lives
"Clocks don't go backwards with NTP"Factually wrong; misses the correlated-failure case entirely
Promises strictly ordered global IDs without a sequencerConfuses k-sorted with total order; consumers will lose data
Exposes sequential IDs publiclyEnumeration and volume leakage; IDOR amplification
Uses UUIDv4 strings as clustered PKs at billions of rows without commentMissing storage-engine consequence of a key choice

5.4 Common False Positives#

  • Reciting the Snowflake layout ≠ understanding ID generation. The layout is public; the lease and clock handling are the design.
  • Knowing UUID versions by number ≠ judgment. The question is which property (order, opacity, size) you are trading and who pays.
  • Collision-probability math ≠ uniqueness analysis. Random collisions at 122 bits never happen; operational collisions (cloned worker IDs, reissued timestamps) happen every year somewhere.
  • "We'll use a ticket server like Flickr" ≠ dependency thinking — unless the candidate sizes batches and states the outage tolerance.

6. Interview Flow & Pivots#

6.1 Typical 45-Minute Shape#

PhaseTimeGoal
Framing & intents0–4 minSplit internal/public; commit to PK intent; state ordering guarantee
Format & API4–7 min64-bit layout, library API, string encoding
Architecture7–11 minIn-process generation, lease service, backstop
Deep dive: worker identity11–20 minLeases, self-fencing, collisions
Deep dive: clock20–28 minRegression policy, correlated failure
Deep dive: format / storage / leakage28–38 minIndex locality, one-way doors, public IDs
Multi-region / evolution38–43 minRegion bits, lease store placement, migration
Wrap-up43–45 minDeferred items, reserved bits

6.2 How Interviewers Pivot — And What They're Testing#

PivotWhat They're TestingStrong Response
"Make it 10× bigger"Whether you know which field saturates"Sequence per worker saturates at 4M/s — we add workers, not bits. Worker space (1,024) saturates first under autoscaling churn; leases must expire promptly."
"We need strict global order"Precision about ordering"That's a sequencer per ordering domain. What consumer needs it? Usually a cursor that tolerates a skew overlap is enough."
"Make it work offline on phones"Coordination-free uniqueness"Devices can't hold leases — UUIDv7 on device, server keeps its own PK."
"Customers complain IDs reveal signup dates"Leakage"Public IDs become opaque; internal stays time-ordered."
"We're going multi-region"Topology in IDs"Region bits from the reserved space; leases per region from disjoint worker ranges; no cross-region coordination per ID."
"Why not just UUIDs?"Storage tradeoffs"Fine for most tables. For the 3 tables with 10B+ rows, 8 extra bytes × 6 indexes is real money and memory."

6.3 What to Deliberately Skip#

  • Deriving bit math on the board — state it.
  • Base62/base32 alphabet details — mention once.
  • Exact NTP algorithm internals — name slew vs step, that's enough.
  • Comparing every UUID version (v1, v3, v5, v6, v8) — v4 and v7 cover the decision.
  • Short-code generation — point to URL Shortener.

6.4 Follow-Up Questions to Expect#

  1. "How does a new instance get a worker ID, and what if two get the same one?"
  2. "What does your generator do when the clock goes back 50ms? 5 seconds?"
  3. "Your ID service is down — what happens to writes?" (Answer: there isn't one on the hot path; describe lease-store outage tolerance.)
  4. "Can I use these IDs for cursor pagination? What breaks?"
  5. "What does a competitor learn from your order IDs?"
  6. "How would you change the epoch or add region bits after launch?"
  7. "Why not UUIDv4 for everything?"

7. Active Drills#

Drill 1: The Opening#

Prompt: "Design a distributed ID generator."

Staff Answer

"First, what's the ID for? Primary keys want small and time-ordered for index locality. Public IDs want opaque and non-enumerable. Ordering keys need a precise monotonicity guarantee. Routing keys embed location. One ID can't do all four well, so I'll split internal from public. I'll design the internal primary key: 64-bit, k-sorted, generated in-process by a library — no network hop per ID. The two assumptions that carry the design are worker-ID uniqueness, which I'll guarantee with leases from etcd, and clock monotonicity, which I'll guard with a regression policy. Public IDs get 128 random bits with a type prefix. Then I'll cover what the format commits us to."

Why this is L6:

  • Asks intent before format and commits to a split that removes a class of incidents
  • Names the two load-bearing assumptions up front instead of waiting to be probed
  • Chooses a library over a service and says why (latency, dependency tier)

What L7 adds:

  • Asks how many ID schemes exist and which systems parse them — consolidation may be the real project
  • Frames the format as an org-wide registry entry with a review gate, not a team decision
  • Names the duplicate-ID rate as an SLO someone is accountable for
❌ Common L5 Trap

"I'll use Twitter Snowflake: 1 sign bit, 41 bits of timestamp, 10 bits of machine ID, 12 bits of sequence. That gives 69 years and 4,096 IDs per millisecond per machine…"

Why this misses: Correct, and it answers a question nobody asked yet. The interviewer's next three questions — where does machine ID come from, what if the clock goes back, can customers see this — each require the candidate to retrofit what the Staff candidate said in the first minute.


Drill 2: Make the Layout Concrete#

Prompt: "Give me the exact bit layout and justify every field."

Staff Answer

"64 bits, top bit zero so it's positive in signed bigint. 41 bits of milliseconds from a custom epoch of 2026-01-01 — 69 years of range, and starting recent avoids wasting decades. Then 2 region bits and 1 format-version bit, all zero today — reserved, because adding them later is a migration. 10 bits of worker ID — 1,024 concurrent leases, which covers ~300 instances with headroom for rolling-deploy overlap. That leaves 9 bits of sequence — 512 per ms, 512K IDs/s per worker. Our peak is 500K/s across 300 instances, so ~1.7K/s per instance — 300× headroom. Timestamp is in the high bits so numeric order is time order. Encoded as a decimal string in JSON."

Why this is L6:

  • Trades sequence bits for reserved bits deliberately, with the arithmetic to prove headroom
  • Chooses the epoch as a one-way door, not a default
  • Specifies wire encoding as part of the format

What L7 adds:

  • Registers the layout (epoch, widths, version) in an org registry so no second team picks a colliding epoch with the same layout
  • Specifies the deprecation path for the version bit: what version 1 will mean and who approves it

Drill 3: The Clock Goes Backwards#

Prompt: "NTP steps the clock back 800ms on one host. Walk me through what your generator does."

Staff Answer

"On the next call, now is 800ms less than last_ts. The policy is tiered. Under 5ms I'd spin until the clock passes last_ts — callers see a few ms. Between 5ms and 1s — this case — I continue on a logical clock: keep issuing with ts = last_ts, incrementing the sequence; when the sequence overflows, advance the logical ms by one. IDs run up to 800ms 'in the future' until the wall clock catches up — uniqueness preserved, ordering skewed by at most 800ms on that host. I increment id_gen.clock_regression_total and record the magnitude. Over 1s, I stop issuing, fail health checks so the LB drains the instance, and page — that's an infrastructure problem. Separately, last_ts is persisted every second, so a restart during the regression resumes from the persisted value, not the wall clock."

Why this is L6:

  • Has a policy with thresholds rather than a single behavior
  • Distinguishes uniqueness (must hold) from ordering (may degrade, bounded)
  • Covers the restart case, which is where naive in-memory guards fail

What L7 adds:

  • Recognizes clock events are correlated across the fleet and adds a fleet-wide circuit: if > 5% of instances refuse simultaneously, switch policy to logical-clock continuation rather than draining 5% of capacity at once
  • Pushes the root cause to the time-infrastructure owner: slew-only after boot, leap smear, offset SLO

Drill 4: Worker IDs in Kubernetes#

Prompt: "You're on Kubernetes with HPA. How does each pod get a unique worker ID?"

Staff Answer

"Not from the hostname hash — 200 pods in 1,024 slots collide with ~18% probability. Not from the pod IP — IPs are recycled. StatefulSet ordinals are unique within one StatefulSet but not across clusters or across Deployments. So: on startup the library scans for a free key under /idgen/workers/{0..1023} in etcd and does a transaction 'create if not exists' with a 30s lease attached. It renews every 10s. If renewal hasn't succeeded for 20s, the generator stops issuing — it can't prove it still owns the ID, and etcd will grant it to someone else at 30s. On graceful shutdown it revokes the lease so the ID returns to the pool immediately. Pool exhaustion is alerted at 80% occupancy because rolling deploys briefly double the pod count."

Why this is L6:

  • Rejects the plausible-but-wrong derivations with the specific failure for each
  • Self-fences with a margin, which is the part most lease designs get wrong (the same lease discipline as in Distributed Coordination)
  • Sizes the pool for deploy overlap

What L7 adds:

  • Uses one identity-lease service for ID generators, schedulers and leader election rather than each team implementing etcd leases
  • Partitions the worker space by cluster/region in the registry so clusters can never contend

Drill 5: Dependency Down — The Allocator#

Prompt: "You chose segment allocation for dense invoice numbers. The allocator DB is down for 2 minutes. What happens?"

Staff Answer

"Each node holds its current segment plus a prefetched next one. Invoice creation peaks at 300/s across 6 nodes — 50/s per node. With step 5,000, each node has up to 10,000 IDs buffered: 200 seconds of headroom. A 2-minute outage is invisible. I'd size step so buffered time ≥ 2× the allocator's worst observed failover, and alert when segment_remaining drops below 60s of consumption. The cost: if a node crashes, up to 10,000 numbers are lost as a gap. If the requirement is 'no gaps' — which some tax regimes impose on invoice numbers — then this is the wrong mechanism entirely: I'd assign the number in the same transaction that commits the invoice, per tenant, from a counter row, and accept that invoice creation serializes per tenant."

Why this is L6:

  • Turns 'is the dependency down?' into buffer arithmetic
  • Calls out that 'dense' and 'gap-free' are different requirements with different mechanisms
  • Names who pays for gap-free: per-tenant serialization

What L7 adds:

  • Asks legal/finance which jurisdictions actually require gap-free numbering before building it everywhere
  • Separates the regulated 'invoice number' (a business identifier) from the internal PK so the regulation doesn't constrain the storage layer

Drill 6: The Hot Generator#

Prompt: "One ingestion service needs 20M IDs per second. Your layout gives 512K per worker."

Staff Answer

"I wouldn't widen the sequence — that's a format change for every consumer. 20M/s ÷ 512K ≈ 40 worker IDs. The ingestion service runs ~40 instances anyway, or one instance takes multiple worker leases and round-robins. But first: does it need our format? 20M IDs/s for telemetry probably means an event ID that's never a PK in OLTP — UUIDv7 generated client-side costs nothing and needs no leases. If it does land in our OLTP store, 40 leases out of 1,024 is 4% of the space; I'd give it a reserved range so autoscaling elsewhere can't starve it."

Why this is L6:

  • Scales by worker count, not by format change
  • Questions whether the high-volume stream belongs in the same ID scheme at all
  • Protects the shared worker space from one consumer

What L7 adds:

  • Treats the worker-ID space as a capacity pool with quotas per consumer, reviewed quarterly like any shared resource

Drill 7: Multi-Tenant Leakage#

Prompt: "We're a B2B SaaS. Enterprise customers can see their own object IDs. What's the risk?"

Staff Answer

"If customers see internal k-sorted IDs, tenant A learns our global creation rate by watching gaps between their own IDs — every gap is someone else's objects. They also learn object creation times. For B2B that's commercially sensitive — a customer can estimate our growth quarter over quarter. Public IDs per object type, opaque, 128 random bits, base62, prefixed: inv_3XkP…. If a customer needs ordering, give them created_at and a cursor token — opaque, signed, containing (created_at, internal_id) — not raw IDs. Tenant-scoped sequential numbers like 'Invoice #1042' are fine as display numbers because they reveal only the tenant's own volume to the tenant."

Why this is L6:

  • Identifies cross-tenant leakage through gaps, not just enumeration
  • Separates display numbers, public IDs and internal IDs with a reason for each
  • Offers opaque cursors so ordering doesn't require exposing internals

What L7 adds:

  • Puts leakage classification into the ID registry and requires security review for any new ID type exposed externally

Drill 8: Build vs Buy#

Prompt: "Should we build an ID service, use a library, or just use UUIDv7 from the database?"

Staff Answer

"Default to the least machinery. Where 16 bytes is acceptable — most tables — UUIDv7, generated in the app or the database (PostgreSQL 18 ships a uuidv7() function; older versions can use an extension or app-side generation). No leases, no clock policy affecting uniqueness. For the few tables at 10B+ rows where 8 bytes per key across 6 indexes matters, a library-based 64-bit generator with leased worker IDs. A network ID service only for stacks that can't embed the library — and it serves batches, never single IDs on a request path. I would not build a standalone service first: it turns a 100ns local call into a tier-0 dependency that every write path must survive losing."

Why this is L6:

  • Chooses per-table rather than one global answer
  • Ranks options by operational burden, not elegance
  • Rejects the 'ID service' as a default with a dependency argument

What L7 adds:

  • Prices it: the library + lease service is ~0.5 FTE ongoing; a network ID service is ~1.5 FTE plus a tier-0 on-call rotation
  • Commits to retiring the legacy ticket server on a date, with a migration SLO

Drill 9: Changing the Format Without an Outage#

Prompt: "We used Unix epoch and 10 worker bits; we need region bits now. How do we change the format?"

Staff Answer

"First, inventory consumers: who parses IDs (timestamp extraction, shard routing), who sorts them, who stores them in fixed-width fields. The new format must sort after every old ID, or consumers using ID order for cursors break — so the new epoch/layout must produce IDs numerically greater than any old ID at cutover. I'd set the version bit to 1 for new IDs, which, if it sits above the timestamp, guarantees new > old. Then: ship a parse() that understands both versions; migrate every consumer that parses IDs; shadow-generate new-format IDs and validate; flip generation per service behind a flag; keep old IDs forever — we never rewrite stored IDs. Duration: 1–3 quarters, dominated by consumer audits."

Why this is L6:

  • Leads with consumers, not the generator
  • Preserves sort order across the cutover — the subtle correctness requirement
  • Never rewrites stored IDs

What L7 adds:

  • Uses the incident to justify the ID registry and reserved bits so the next change is a config flip
  • Sets a policy that consumers may not parse internal IDs, enforced by deprecating parse() outside debugging tools

Drill 10: Multi-Region#

Prompt: "We're going active-active in three regions. What changes?"

Staff Answer

"Per-ID generation stays local — no cross-region call ever. Two things change. Worker identity: each region gets its own lease store and a disjoint slice of the worker space, either via the 2 reserved region bits or by partitioning the 1,024 worker IDs (e.g., 0–339, 340–679, 680–1,019). That guarantees uniqueness even if regions are partitioned from each other indefinitely. Ordering: cross-region skew is tens of ms, so k-sorting degrades to ~50ms globally — consumers needing tighter order must order per region. Data placement: the region bits say where the ID was minted, not where the data lives now; routing must use a directory, because data moves."

Why this is L6:

  • Keeps generation local; partitions identity space so partitions can't cause duplicates
  • Quantifies ordering degradation
  • Separates 'minted in' from 'lives in'

What L7 adds:

  • Coordinates with data-residency policy: if regulations move data between regions, embedded region bits must never be used for compliance decisions
  • Plans region-cell expansion (4th, 5th region) within the reserved bit budget, and documents the ceiling

8. Deep Dive Scenarios#

Deep Dive 1: Peak-Traffic Incident — Generator Convoy During a Launch#

Context: During a product launch, write p99 on the order service jumps from 15ms to 900ms on 30% of instances. CPU is fine, the DB is fine. Profiling shows threads blocked inside IdGenerator.next(). You're escalated.

Questions to Surface First:

  • Is the generator waiting on the clock (regression) or on the sequence (exhaustion) or on a lock?
  • Did a bulk job or new feature start calling next() in a loop on the same instances?
  • Are affected instances clustered on particular hosts (clock event) or particular deployments (code change)?

Typical L5 Approach: Adds more instances so load per instance drops, and widens the timeout. Latency improves but the root cause — a backfill job generating 4M IDs/s on shared request-serving instances, convoying behind a synchronized next() — remains, and returns at the next launch.

Staff Approach: Reads id_gen.seq_exhausted_total and lock-wait metrics: exhaustion is spiking on exactly the instances where the launch's backfill job is co-located. Moves the backfill to dedicated instances with their own worker IDs and nextBatch(), and ships the lock-free CAS implementation. Adds a guard: next() callers exceeding 1M IDs/s per instance get a log warning and a metric.

Principal Approach: Sees the incident as a workload-isolation failure, not a generator bug: batch and serving traffic share capacity with no isolation contract. Makes 'bulk ID consumers use dedicated worker-ID ranges and the batch API' part of the ID standard, adds it to the launch-readiness checklist, and has the platform team publish per-consumer ID-rate dashboards so the next team sees its own impact before launch.

Staff Approach — Full Reasoning
PhaseWhat to Do
Immediate (0–5 min)Confirm blocked threads in next(). Check id_gen.seq_exhausted_total vs id_gen.clock_regression_ms. If exhaustion: pause the backfill job.
TriageCorrelate affected instances with job placement. Confirm no clock events (ntp.offset_ms flat).
Quick fixPause or throttle the backfill; restart affected instances if thread pools are wedged.
GuardrailsBackfill restarts on dedicated instances with separate worker IDs and nextBatch(10_000).
Post-mortemWhy was a 4M IDs/s job co-located with serving? Why did next() convoy? Ship lock-free implementation.

Metrics to Watch: id_gen.seq_exhausted_total, id_gen.next_latency_p99, jvm.thread.blocked_count, orders.write_latency_p99

Organizational Follow-up: Launch-readiness checklist gets an "ID consumption estimate" line. Platform team publishes per-consumer ID rate.

Ownership Question: "Who owns the fix — the order team or the ID platform?" Staff answer: Both, split cleanly. The order team owns workload placement (backfills don't share serving capacity). The ID platform owns making next() non-convoying and exposing the per-consumer rate. Neither fix alone prevents recurrence.

Key Takeaway: "A generator that is fast on average can still convoy. Isolate bulk consumers and measure ID rate per consumer."

What clears the Staff bar:

  • Uses generator-specific metrics to separate exhaustion from regression in minutes
  • Fixes both workload placement and implementation
  • Names the split ownership explicitly

Deep Dive 2: Silent Failure — Duplicate IDs Found Three Weeks Later#

Context: Data engineering reports that the events warehouse has 41,000 rows sharing an event_id with a different payload, all from the past 22 days. The events table in OLTP has no unique constraint. Nobody was paged.

Questions to Surface First:

  • What changed 22 days ago in how worker IDs are assigned (deploy, new cluster, config)?
  • Which worker_id values appear in duplicates, and which hosts/pods held them?
  • Which downstream systems dedupe by event_id — and therefore dropped real data?

Typical L5 Approach: Finds that a new cluster was stood up 22 days ago with a copied config that hard-codes worker IDs 0–63, fixes the config, and writes a script to re-key the duplicate rows. Closes the incident.

Staff Approach: Fixes the config, then asks why the failure was silent. Three gaps: worker IDs could come from config at all; the events table had no unique constraint; no metric compares lease holders. Removes the config path from the library, adds the unique constraint (accepting ~3% write cost), adds a lease-conflict auditor, and quantifies downstream damage: every consumer that deduped by event_id lost data and must be backfilled from the raw log.

Principal Approach: Treats it as a class problem: any coordination-free generator with a non-leased identity can silently corrupt data, and there are likely other generators in the org with the same weakness (job schedulers, sharded counters). Commissions an inventory of every generator and its identity source, mandates leases from the shared identity service, and sets an org SLO — zero duplicate-key events, with each one reviewed like a security incident. Funds it by pricing the downstream reprocessing (weeks of data-engineering time) against the 0.5 FTE to run the shared lease service.

Staff Approach — Full Reasoning
PhaseWhat to Do
Immediate (0–5 min)Identify colliding worker IDs; stop generators on the cloned cluster (fail closed); confirm duplicates stop.
TriageMap every downstream consumer of event_id; classify: dedup-by-ID (data loss) vs append (duplicates).
Quick fixRe-lease worker IDs for the new cluster from etcd; restart.
GuardrailsUnique constraint on events table; lease-conflict auditor; CI test that generators refuse to start without a lease.
Post-mortemWhy did a config path exist? Why no constraint? Why no conflict metric? Backfill plan for each affected consumer.

Metrics to Watch: db.duplicate_key_errors, id_gen.worker_lease_conflicts, id_gen.generators_without_lease (must be 0), warehouse.duplicate_event_ids_daily

Organizational Follow-up: Inventory all ID generators and their identity sources. Make the leased path the only path.

Ownership Question: "Who decides whether to add the unique constraint, given its write cost?" Staff answer: The events data owner decides, but the ID platform's standard requires it for any table keyed by a generated ID. The 3% write cost is the price of turning a 22-day silent failure into a 1-minute page; exceptions need sign-off from the data owner's director.

Key Takeaway: "The cost of a duplicate ID is paid by the consumer that dedupes it. Make duplicates loud at the source."

What clears the Staff bar:

  • Asks 'why was it silent?' before 'what broke?'
  • Traces downstream data loss caused by deduplication
  • Removes the unsafe path rather than fixing the instance

Deep Dive 3: Large-Customer Onboarding — Importing 2 Billion Legacy Records#

Context: An acquired company's 2B records must be imported. Their records already have 64-bit IDs from their own Snowflake-like scheme with a different epoch, and their customers have those IDs in URLs and integrations.

Questions to Surface First:

  • Do their customers reference these IDs externally? (Then they are public IDs and must be preserved verbatim.)
  • Do their IDs overlap numerically with ours? Do they sort before or after ours?
  • Will new records for those customers use our scheme or theirs?

Typical L5 Approach: Re-keys the 2B records with new IDs from our generator during import and stores the old ID in a column. Breaks every external integration that holds old IDs unless every lookup path is updated.

Staff Approach: Separates concerns: assign new internal IDs from our generator (via nextBatch on a dedicated worker range — 2B IDs at 512K/s per worker is ~65 minutes on one worker, or ~4 minutes on 16), and preserve legacy IDs as public identifiers in a legacy_id column with a unique index and a lookup path. External APIs accept both forms for those tenants. New records get our public ID format.

Principal Approach: Uses the acquisition to write the "foreign ID" chapter of the identifier standard: imported identifiers are always public aliases, never internal keys; alias resolution is a platform service; aliases have a documented retention policy. Plans the eventual sunset of legacy IDs with the acquired customers' contracts in mind — a one-way door owned by the business, not engineering.

Staff Approach — Full Reasoning
PhaseWhat to Do
ImmediateAnalyze legacy ID ranges for overlap with ours; confirm external references.
TriageDecide alias model: legacy IDs become public aliases with unique index.
Quick fixImport with fresh internal IDs from a dedicated worker range; build alias lookup.
GuardrailsRate-limit import to protect DB; verify every legacy ID resolves post-import (2B-row checksum).
Post-mortem / planDocument the alias pattern; set sunset policy for legacy IDs.

Metrics to Watch: import.rows_per_sec, alias.lookup_miss_rate, db.replication_lag, id_gen.worker_range_usage{range="import"}

Organizational Follow-up: Identifier standard gains a "foreign IDs" section; M&A integration checklist includes ID mapping.

Ownership Question: "Who owns legacy ID resolution after the import team disbands?" Staff answer: The ID platform owns the alias service; the acquired product team owns the deprecation timeline, signed off by account management because it touches customer contracts.

Key Takeaway: "Imported IDs are public aliases, never internal keys."

What clears the Staff bar:

  • Distinguishes internal and public identity during migration
  • Sizes the batch generation with numbers
  • Plans for long-lived alias ownership

Deep Dive 4: Post-Mortem — 40-Minute Write Outage From a Leap-Second-Style Clock Event#

Context: A time-source misconfiguration caused 35% of hosts to step their clocks back 1.2s simultaneously. Generators were configured to refuse IDs on any regression > 1s. Load balancers drained 35% of the fleet within 15s; the remainder overloaded; the write path was degraded for 40 minutes.

Questions to Surface First:

  • Why did 35% of hosts receive the same bad time — shared NTP source? Same hypervisor pool?
  • Was "refuse on > 1s" evaluated against correlated failure, or only single-host failure?
  • Why 40 minutes — why didn't instances re-admit after the clock passed last_ts 1.2s later?

Typical L5 Approach: Raises the refusal threshold to 10s and moves on. This trades a loud outage for a larger silent ordering anomaly, and the next correlated event of 11s causes the same outage.

Staff Approach: Finds the 40-minute tail: instances that refused also failed readiness, and readiness only recovered on restart, which re-ran the "clock behind persisted last_ts" check — a refusal loop. Fixes: refusal is temporary and self-clearing once now > last_ts; for regressions between 1s and 10s, an instance cannot tell locally whether the event is isolated or fleet-wide, so it continues on the logical clock by default and pages; it refuses only above 10s. Fixes the NTP configuration to slew after boot.

Principal Approach: Treats it as a correlated-failure design gap: per-instance safety policies that are individually correct can be collectively catastrophic. Establishes a fleet clock-health signal (fraction of hosts with offset > 100ms) that every clock-sensitive system — ID generators, lease holders, token validators — consumes, and an org rule that safety policies triggered by shared infrastructure must degrade rather than drain. Adds time-source failure to the quarterly game-day calendar with the compute platform owning it.

Staff Approach — Full Reasoning
PhaseWhat to Do
Immediate (0–5 min)Flip generators to logical-clock continuation via feature flag; stop draining.
TriageIdentify the time source; confirm affected host set; verify no duplicates (logical continuation preserves uniqueness).
Quick fixCorrect the NTP source; restart wedged instances.
GuardrailsRefusal self-clears; refusal threshold raised to 10s; slew-only NTP after boot.
Post-mortemCorrelated failure review of every per-instance safety policy that depends on shared infra.

Metrics to Watch: fleet.clock_offset_hosts_over_100ms_pct, id_gen.refusals_total, lb.healthy_instances_pct, id_gen.logical_clock_ahead_ms

Organizational Follow-up: Time-source changes go through change management; game day for NTP failure.

Ownership Question: "Who owns the decision to refuse versus continue?" Staff answer: The ID platform owns the default policy; the compute platform owns the fleet clock-health signal that can override it. The override is pre-approved, not decided during the incident.

Key Takeaway: "A safety check that drains instances is a correlated-failure amplifier when the cause is shared infrastructure."

What clears the Staff bar:

  • Finds the reason for the long tail, not just the trigger
  • Rejects 'raise the threshold' as the fix
  • Distinguishes uniqueness preservation from ordering preservation under stress

Deep Dive 5: Multi-Region Expansion#

Context: The company is adding two regions. The current generator uses a single etcd cluster in us-east for worker leases and 10 worker bits with no region bits.

Questions to Surface First:

  • Will regions be able to mint IDs while partitioned from us-east? (They must.)
  • Do any consumers rely on global ID order across regions?
  • How many generators per region at peak, including deploy overlap?

Typical L5 Approach: Keeps the single etcd in us-east; new regions acquire leases across the WAN. A transatlantic partition then blocks new instances in eu-west from starting and, after the safety margin, stops existing ones.

Staff Approach: Per-region lease stores with disjoint worker ranges (or the reserved region bits). No cross-region dependency for minting. Documents that global ordering degrades to cross-region skew (~50ms) and audits consumers that use ID order across regions — moving them to per-region cursors or to (created_at, id) with an overlap window.

Principal Approach: Ties ID topology to the org's regional cell strategy: each cell owns a slice of identity space, and the registry records allocations so a future sixth region doesn't collide. Notes that region bits encode minting location, and makes it a written rule that no compliance or residency decision may read them. Budgets the cross-region consumer audit as part of the multi-region program, not as an ID-team task.

Staff Approach — Full Reasoning
PhaseWhat to Do
PlanAllocate worker ranges per region; stand up per-region etcd.
Triage risksInventory cross-region ID-order consumers.
RolloutNew regions first with their ranges; migrate us-east to its range by lease rotation (no restarts needed).
GuardrailsAlert on any lease acquired outside the region's range.
ReviewMeasure cross-region k-sorting bound; publish it.

Metrics to Watch: id_gen.lease_acquire_latency{region}, id_gen.worker_range_usage{region}, id_gen.out_of_range_lease_total, cross-region ntp.offset_ms

Organizational Follow-up: Registry entry for region allocations; multi-region readiness checklist item.

Ownership Question: "Who owns the worker-range allocation table?" Staff answer: The ID platform team, with changes reviewed like any capacity allocation. Product teams request ranges; they never pick them.

Key Takeaway: "ID minting must survive a region partition. Partition the identity space, not the request path."

What clears the Staff bar:

  • Eliminates cross-region dependency for minting
  • Quantifies ordering degradation and audits consumers
  • Separates minting location from data location

9. Level Expectations Summary#

After studying this case study, you should be able to:

  • Name the four ID intents and explain why internal and public IDs should be separate columns
  • State, in one sentence, which assumption carries uniqueness and which carries ordering in any ID scheme
  • Design worker-ID leasing with TTL, renewal and a self-fencing margin, and explain why hostname/IP derivation fails
  • Give a tiered clock-regression policy with thresholds, and explain why per-instance policies can fail collectively
  • Explain the B-tree consequence of random vs time-ordered keys, with page-fill and index-size numbers
  • Define k-sorted precisely and design a cursor that tolerates it
  • Describe what an ID leaks (time, volume, topology) and how opaque public IDs prevent it
  • Identify epoch, layout, wire encoding and embedded topology as one-way doors, and plan a format migration
  • Size segment allocation buffers against allocator failover time

The Bar for This Question#

Mid-level (L4): Knows UUIDs and auto-increment, maybe Snowflake by name. Can explain why a single auto-increment doesn't survive sharding. Doesn't reach worker identity or clock behavior without prompting.

Senior (L5): Produces a correct Snowflake layout with the arithmetic, mentions NTP and machine IDs, and may compare UUIDs. The design works on a whiteboard. Under probing, answers about worker-ID assignment and clock regression are improvised, and the public-exposure question is a surprise.

Staff+ (L6): Opens with intent and the internal/public split. Places the uniqueness guarantee in worker leases with self-fencing and a unique-constraint backstop, and has a clock policy with thresholds and an owner. Connects key choice to index locality with numbers, defines ordering precisely, names leakage, and treats the format as a one-way door with reserved bits. Spends under five minutes on bits. The interviewer should learn something from the answer — typically the correlated clock-failure case or the downstream-dedup-turns-duplicates-into-data-loss insight.


10. Staff Insiders: Controversial Opinions#

10.1 "You Probably Don't Need an ID Service"#

EvidenceDetail
Generation cost~100ns in-process vs 0.5–2ms over the network
Dependency tierAn ID service on the write path is tier-0: its availability caps every writer's
What actually needs coordinationWorker identity — once per instance lifetime, renewed every ~10s
UUIDv7Removes even that, for 8 extra bytes

The Staff position: Ship a library and a lease mechanism. Offer a batch endpoint for stacks that can't embed the library. A synchronous per-ID network service is almost always a design smell.

Why this matters in interviews: Candidates who draw an "ID Service" box with replicas and a load balancer are solving availability for a problem they created. Saying "this is a library" saves five minutes and signals judgment.

10.2 "Auto-Increment Is Fine for Longer Than You Think"#

EvidenceDetail
PostgreSQL sequencesNon-transactional, cacheable (CACHE 100), not a lock bottleneck at thousands/s
Most tablesNever shard; most databases never exceed one primary's write capacity
Migration cost laterA bigint PK migrates to a Snowflake-style bigint without a type change — new IDs just need to be larger than the max existing ID

The Staff position: Use the database sequence until a sharding decision forces otherwise, but keep it out of public URLs from day one. The expensive mistake is not the sequence — it is exposing it.

Why this matters in interviews: Showing you'd not build something is a stronger signal than building it well.

10.3 "Never Expose Your Primary Key"#

EvidenceDetail
EnumerationSequential IDs make IDOR bugs exploitable at scale
Volume leakageTwo IDs and two dates estimate your business volume
Format lock-inOnce customers store your PK, its format is a public contract forever
Typed prefixesSelf-describing IDs (cus_, ord_) cut support time and catch type-confusion bugs

The Staff position: Public IDs are a separate column, opaque, prefixed. The second unique index costs ~5% of write throughput and buys format freedom for the internal key forever.

Why this matters in interviews: It converts a privacy question into a data-modeling decision you already made.

10.4 "Strict Global Ordering Is a Requirement Smell"#

EvidenceDetail
CostRequires a single sequencer per ordering domain (throughput ceiling, failover gap) or bounded-uncertainty clocks (Spanner-style commit wait)
What consumers usually needPer-entity or per-partition order, or a cursor that never skips
Cursor fixRe-read an overlap window of max_skew and dedupe

The Staff position: Ask which consumer needs strict global order and why. Usually the answer is per-key order (use a partitioned log) or "don't skip rows in pagination" (use an overlap).

Why this matters in interviews: Pushing back on a requirement with a cheaper, sufficient alternative is the Staff move.

10.5 "Embedding Physical Shard IDs Is a Year-3 Migration You Chose in Year 0"#

EvidenceDetail
ReshardingPhysical shard in ID → every ID wrong after reshard, or a permanent translation layer
Logical shardsFixed large space (4K–8K) mapped to physical via a tiny directory — Instagram's approach
Directory costA few KB, cached everywhere, changes rarely

The Staff position: Logical shards or nothing. Topology belongs in a directory, not in billions of stored references.

Why this matters in interviews: It is the single best example of a one-way door hidden inside a "simple" bit layout.


11. The Principal Lens (L7)#

Why L7 Sees This Problem Differently#

A Staff engineer designs a generator. A Principal engineer finds that the org already runs six: a Flickr-style ticket server from 2014 that three services still call, two hand-rolled Snowflake variants with different epochs, UUIDv4 strings in the newer services, a Leaf-style allocator in payments, and a Mongo ObjectId leak into a public API. The generators are cheap; the consumers are the liability — mobile clients storing IDs as doubles, analytics jobs extracting timestamps from bits, partner integrations with 32-bit columns, support tooling that parses prefixes. At L7 the problem is identifier governance: one identity-lease primitive, a registry of formats, a public-ID standard, and a retirement plan for the legacy schemes.

🧭 Principal Move: "Before we design a generator, I'd like a list of every ID scheme in production and every system that parses one. My bet is the highest-leverage work is one lease service, one public-ID standard, and retiring the ticket server — not a new bit layout."

The Org-Level Fault Line#

Central ID service vs shared library vs per-team generators.

OptionWhat WorksWhat BreaksWho Pays
Per-team generatorsTeams move fast; formats fit local needsDuplicate-ID risk from ad-hoc worker identity; 4–6 incompatible formats; every consumer special-casesData consumers, security, on-call for silent corruption
Central network ID serviceOne implementation; easy to auditTier-0 dependency for every write; ~1ms tax; platform team on the critical path of every incidentEvery product team (latency, availability); platform on-call
Shared library + central identity leasing + format registry (L7 default)No per-ID dependency; uniqueness centrally guaranteed; formats governedLibrary version skew across languages; needs a lease service and a registryPlatform: ~1 FTE build, ~0.5 FTE run; teams: adopt library

The deciding question: how many languages must the library support? If two, the library wins outright. If six, a sidecar or batch-endpoint fallback covers the long tail while the library covers the 80%.

Cost Model#

Assumptions: loaded engineer cost ~$25K/month; etcd cluster of 3 small nodes ~$300–$600/month per region; storage at ~$0.10/GB-month for primary SSD with 3× replication; buffer-pool memory at ~$4/GB-month.

ScaleArchitectureInfra $/monthHeadcountOn-Call Load
~5K IDs/s, 1 region, 1 DBDB sequences + opaque public IDs~$0 incremental~0 (part of data modeling)None
~500K IDs/s, 1–2 regions, sharded64-bit library + etcd leases + UUIDv7 for long tail~$1K–$2K~1 FTE build for 2 quarters, then 0.5 FTEShares platform rotation; ~1 page/quarter
~20M IDs/s, 5 regions, 50+ servicesLibrary in 4 languages, per-region lease stores, registry, alias service for acquisitions~$5K–$10K2–3 FTE (library, leasing, registry/aliases)Own runbook; clock and lease game days twice a year

The line that matters to leadership: The infrastructure is a rounding error. The money is in two places: storage — 8 extra bytes per key × 6 indexes × 50B rows ≈ 2.4 TB of index, roughly $1K/month in disk and far more in buffer-pool memory if it must be hot — and migrations: a format change touching 40 consumers costs 2–4 engineer-quarters (~$150K–$300K). Reserved bits and a registry are how you avoid paying the second one.

The 3-Year Evolution Path#

Diagram: The 3-Year Evolution Path

Each step is triggered by an event. Building per-region lease stores before the second region exists is wasted platform credibility.

One-Way Doors vs Two-Way Doors#

DecisionDoorReversal CostWhy
Generator implementation (lock-free vs mutex)Two-wayDaysHidden behind next()
Lease store (etcd vs ZooKeeper vs DB table)Two-wayWeeksHidden behind the identity-lease API
EpochOne-wayQuartersChanging it reorders or collides with existing IDs
Bit layoutOne-wayQuartersEvery parser, router and sorter depends on it
Wire encoding (string vs number)One-wayYearsExternal clients hard-code it
Public ID format and prefixesOne-wayNever reversed in practiceCustomers store them indefinitely
Physical shard in IDOne-wayMulti-quarter migrationTopology frozen into every reference
UUIDv7 vs 64-bit for a new tableTwo-way-ishWeeks before data; quarters afterCheap to decide per table early

🧭 Principal Insight: Spend design review time in proportion to reversal cost. The generator implementation gets a code review. The epoch, layout, encoding and public prefixes get a design review with API governance, security and the largest consumers in the room.

The Standard I'd Write#

RFC: Identifier Standard (v1)

Scope: Every identifier persisted in a production datastore or exposed through any API, event or export.

Requirements:

  • Internal identifiers MUST be generated by the platform library (64-bit layout v0) or as UUIDv7. New formats require registry approval.
  • Generators with a worker identity MUST obtain it from the platform identity-lease service and MUST stop generating before lease expiry.
  • Tables keyed by generated identifiers MUST have a unique constraint on the key; exceptions require director sign-off.
  • Internal identifiers MUST NOT appear in external APIs, URLs, webhooks or exports. External identifiers MUST be opaque, ≥ 96 bits of entropy, base62, with a registered type prefix.
  • 64-bit identifiers in JSON MUST be serialized as strings.
  • Consumers SHOULD NOT parse internal identifiers; parse() is for debugging tools only.
  • Generators MUST emit id_gen.clock_regression_ms, id_gen.lease_lost_total, and id_gen.seq_exhausted_total.

Exceptions: Filed with the API/data architecture group; decision within 5 business days; expiry ≤ 2 quarters.

Success metrics: Zero duplicate-key events per quarter; 100% of external APIs pass the ID lint within 3 quarters; legacy ID schemes reduced from 6 to 2 within 4 quarters; zero JSON precision bugs reported.

What I'd Tell the VP#

"We have six different ways of generating IDs, and two of them can silently create duplicates — we had one such incident last year that cost three weeks of data cleanup. I'm proposing one shared generator library, one service that guarantees each generator's identity is unique, and a rule that internal IDs never reach customers. It costs about one engineer for two quarters, then half an engineer to run. It removes a class of silent data-corruption incidents and stops leaking our order volume through public IDs. The biggest cost is migrating the old ticket server, which we'll do over three quarters with no customer-visible change."

Principal Interview Signals#

SignalWhat It Sounds Like
Inventories before designing"How many ID schemes exist, and who parses them? The consumers are the liability."
Prices the choice"16-byte keys on our three 50B-row tables cost ~2.4 TB of index; everywhere else UUIDv7 is free."
Centralizes the right thing"Centralize identity leasing, not generation. Generation stays in-process."
Names one-way doors"Epoch, layout, encoding and public prefixes get a design review. The algorithm gets a code review."
Designs for correlated failure"Clock policies must degrade, not drain, when the cause is shared time infrastructure."

Staff answers that L7 interviewers find insufficient:

  • "We'll lease worker IDs from etcd." — correct, but silent on the other five generators in the org that don't.
  • "Public IDs should be opaque." — correct, but no standard, no lint, no review gate, so the next team exposes its PK anyway.
  • "We reserved two bits for the future." — good hygiene, but never priced the migration it avoids or named who allocates those bits.

Appendices

Appendix A: Mechanics in Depth — Every Scheme, Why It's Right or Wrong

A.1 Snowflake-Style Generator (Lock-Free)#

state: atomic int64 packed = (last_ts << SEQ_BITS) | seq

function next():
  loop:
    now = clock_ms() - EPOCH
    old = packed.load()
    last_ts = old >> SEQ_BITS
    seq     = old & SEQ_MASK
    if now < last_ts:
        regression = last_ts - now
        if regression <= 5:            spin; continue
        elif regression <= 10_000:     now = last_ts          # logical continuation
                                       metric clock_regression_ms.observe(regression)
        else:                          raise ClockRegressionRefused   # fast fail, drain
    if now == last_ts:
        if seq == SEQ_MASK:            now = last_ts + 1 if logical else wait_next_ms()
                                       new = (now << SEQ_BITS) | 0
        else:                          new = old + 1
    else:
        new = (now << SEQ_BITS) | 0
    if !lease.valid_with_margin():     raise LeaseLost
    if packed.compare_and_swap(old, new):
        ts, s = new >> SEQ_BITS, new & SEQ_MASK
        return (ts << TS_SHIFT) | (REGION << REGION_SHIFT) | (VERSION << VER_SHIFT)
               | (WORKER << WORKER_SHIFT) | s

Why it's right: no lock convoy; regression policy explicit; lease checked on every call (a cached boolean updated by the watchdog — not a network call). Why it's wrong for some uses: IDs leak time and per-worker volume; requires a lease service.

Persisting last_ts: every 1s write last_ts + 1000 to local disk or the lease record. On start: last_ts = max(now, persisted). This closes the "restart during regression" hole.

A.2 Segment Allocation (Leaf-Style, Double-Buffered)#

-- allocator table
CREATE TABLE id_alloc (biz_tag VARCHAR(64) PRIMARY KEY, max_id BIGINT NOT NULL,
                       step INT NOT NULL, updated_at TIMESTAMP NOT NULL);

-- refill (one row lock, one round trip)
UPDATE id_alloc SET max_id = max_id + step, updated_at = now()
 WHERE biz_tag = :tag RETURNING max_id, step;
-- segment = [max_id - step + 1, max_id]
on next(tag):
  seg = current[tag]
  if seg.remaining < seg.size * 0.8 and next_seg[tag] is empty and not refilling:
      async refill → next_seg[tag]
  if seg.exhausted:
      if next_seg[tag] ready: current[tag] = next_seg[tag]; next_seg[tag] = empty
      else: wait up to 50ms for refill, then raise AllocatorUnavailable
  return seg.take()

Dynamic step: if a segment was consumed in < 15 min, double step (cap at 1M); if > 30 min, halve it. Keeps DB writes roughly constant as traffic grows. Why it's right: no clock, no worker identity, 64-bit dense-ish IDs, allocator DB sees ~1 write per minute per node. Why it's wrong: ordering only within one node; gaps on restart; allocator availability matters after buffers drain.

A.3 Ticket Server (Flickr-Style)#

CREATE TABLE tickets64 (id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
                        stub CHAR(1) NOT NULL DEFAULT '', UNIQUE KEY (stub)) ENGINE=InnoDB;
REPLACE INTO tickets64 (stub) VALUES ('a');
SELECT LAST_INSERT_ID();
-- two servers: auto_increment_increment = 2, offsets 1 and 2 → odd/even IDs

Why it was right (2010): dead simple, dense 64-bit IDs, two servers for availability. Why it's wrong today: one network round trip per ID (or per batch); odd/even servers drift apart so IDs aren't time-ordered across servers; adding a third server means changing increments — a coordinated migration. Use segment allocation instead: same uniqueness model, 1/step the traffic.

A.4 UUIDv7 (RFC 9562)#

| 48 bits unix_ts_ms | 4 bits ver=0111 | 12 bits rand_a | 2 bits var=10 | 62 bits rand_b |

rand_a may hold sub-ms precision or a counter (RFC 9562 method 1/3) for monotonicity within one generator. Right for: most tables where 16 bytes is acceptable; client-side generation; offline devices. Wrong for: public IDs (leaks creation ms); very large tables where key size dominates index memory.

A.5 ULID#

| 48 bits ms timestamp | 80 bits randomness |  → 26 chars Crockford base32, lexicographically sortable

Monotonic mode: within the same ms, increment the random component by 1 instead of regenerating — sortable within a generator, at the cost of making successive IDs predictable from one another. Right for: string-keyed stores where lexicographic order matters (DynamoDB sort keys, S3 prefixes). Wrong for: anywhere a native UUID type would halve storage.

A.6 UUIDv4#

122 random bits. Collision probability for n IDs ≈ n² / 2^123. For 10^12 IDs: ~10^-13. Right for: idempotency keys, public IDs (if 36-char form is acceptable), anything needing no order. Wrong for: clustered primary keys at scale — random insert positions.

A.7 Index Locality — The Numbers#

Key TypeInsert PositionLeaf Page FillWorking SetTypical Effect at 1B+ Rows
Sequential / k-sorted 64-bitRight edge~90%+Last few pagesFlat insert cost
UUIDv7Right edge (ms granularity)~90%Last few pagesFlat, but 2× key size
UUIDv4Random leaf~50–70% after splitsEntire indexInsert throughput degrades sharply once index exceeds buffer pool; 2–5× write amplification commonly reported
Appendix B: Layout Design, Encoding and Public IDs

B.1 Layout Calculator#

Required lifetime L years      → ts_bits ≥ log2(L × 3.156e10 ms)      (50y → 41 bits)
Peak IDs/s per generator R     → seq_bits ≥ log2(R / 1000) + headroom  (500K/s → 9 bits)
Max concurrent generators G    → worker_bits ≥ log2(G × 2 for deploy overlap)
Reserved                       → version 1 bit, region 2 bits
Sign bit                       → 1 (keep positive in signed bigint)
Total must equal 64.

B.2 The Layout Used in This Case Study#

Diagram: B.2 The Layout Used in This Case Study

Placing region and version below the timestamp keeps numeric order equal to time order regardless of region. Placing version above the timestamp instead would guarantee every v1 ID sorts after every v0 ID — useful for a format cutover. Choose deliberately; this is the one-way door.

B.3 Wire Encoding#

ContextEncodingReason
JSON APIsDecimal string "732410982735577088"2^53 precision limit in JavaScript
LogsDecimalgrep-able, matches DB
URLs (internal tools only)DecimalReadability
Sortable string keysZero-padded 19-digit decimal or Crockford base32Lexicographic = numeric order

B.4 Opaque Public IDs#

Option 1 — Random: prefix + "_" + base62(128 random bits) → ~22 chars after prefix. Separate unique index. Simplest, strongest.

Option 2 — Keyed permutation of the internal ID: encrypt the 64-bit internal ID with a 64-bit block cipher or a Feistel network keyed by a secret, then base62 (~11 chars). No second index — decrypt to find the row. Costs: key management; key rotation changes every public ID (so version the key in the prefix, e.g. ord1_…); 64 bits of output is enumerable-resistant only while the key is secret.

OptionIndex CostLengthKey RiskWho Pays
Random 128-bit+1 unique index (~5% write cost)~26 chars with prefixNoneStorage
Keyed permutationNone~15 chars with prefixKey leak re-exposes order; rotation is a migrationSecurity (key custody)

Default: random. Permutation only when a second index is genuinely unaffordable.

Appendix C: Worker Identity Mechanisms — Quick Comparison

C.1 Options#

MechanismUniqueness GuaranteeFailure ModeUse When
Static configHuman disciplineCopy-paste duplicatesNever in autoscaled fleets
Hash(hostname or IP) mod 2^kNone (birthday)~18% collision at 200/1,024Never
StatefulSet ordinalWithin one StatefulSetCollides across clusters/setsCombined with a cluster-ID range
ZooKeeper ephemeral sequential nodeSession-scopedSession expiry while process lives → must stopZK already operated
etcd lease + txn create-if-absentLease-scoped, fenced by revisionLease expiry while process lives → must stopDefault
DB row lease (UPDATE … WHERE expires_at < now())DB atomicityDB clock vs host clock; must use DB timeNo consensus store available

C.2 Lease Lifecycle#

Diagram: C.2 Lease Lifecycle

The margin rule: stop generating at TTL − margin, where margin ≥ max GC pause + clock rate error × TTL + renewal RPC timeout. With TTL 30s, 10s margin is conservative. A process paused for longer than the margin (e.g., a 25s stop-the-world) must re-check lease validity after resuming and before issuing — the watchdog's boolean must be time-stamped, not just set.

C.3 Acquisition Pseudocode (etcd)#

lease = etcd.grant(ttl=30)
for slot in shuffled(range(0, 1024)):          # shuffle to avoid thundering on slot 0
    ok = etcd.txn(
        compare=[create_revision("/idgen/workers/" + slot) == 0],
        success=[put("/idgen/workers/" + slot, pod_uid, lease=lease)])
    if ok: return slot, lease
raise WorkerSpaceExhausted   # alert at 80% occupancy, page at 95%
Appendix D: API Contract and Client Behavior

D.1 Batch Endpoint (for stacks without the library)#

POST /v1/ids:batch
{ "count": 1000, "biz_tag": "orders" }
→ 200 { "ids": ["732410982735577088", ...], "layout_version": 0, "expires_hint_ms": null }
→ 503 { "error": "lease_unavailable", "retry_after_ms": 200 }

Clients cache batches locally and refill at 20% remaining. Never call per request. Retries use jittered backoff (100ms base, 2s cap); a batch fetched but unused is simply a gap.

D.2 Cursor Pagination Over k-Sorted IDs#

-- naive: may permanently skip rows that commit late with smaller IDs
SELECT * FROM events WHERE id > :cursor ORDER BY id LIMIT 100;

-- skew-tolerant: re-read an overlap window equal to the k-sorting bound plus commit latency
SELECT * FROM events
 WHERE id > :cursor - (:overlap_ms << TS_SHIFT)
 ORDER BY id LIMIT 100 + :overlap_estimate;
-- client dedupes by id; cursor advances to max(id) seen

Overlap = max clock skew (~10ms in-region) + max transaction duration (~1s for most OLTP). Rows that commit later than that are still skipped — for strict guarantees, read from the change log (CDC) instead of polling by ID.

D.3 Retry Behavior#

next() failures (LeaseLost, ClockRegressionRefused) are instance-local. Callers should fail the request fast so the load balancer retries on another instance — never spin inside the request on next().

Appendix E: Observability

E.1 Core Metrics#

# Uniqueness
db.duplicate_key_errors{table}             # must be 0 — page on any
id_gen.worker_lease_conflicts              # auditor: same worker_id, two holders
id_gen.generators_without_lease            # must be 0

# Clock
id_gen.clock_regression_ms (histogram)
id_gen.logical_clock_ahead_ms              # how far logical time leads wall clock
id_gen.refusals_total
fleet.clock_offset_hosts_over_100ms_pct

# Capacity
id_gen.seq_exhausted_total
id_gen.worker_space_occupancy_pct
id_gen.segment_remaining_seconds{biz_tag}
id_gen.next_latency_ns p99

E.2 Critical Alerts#

AlertThresholdSeverity
Duplicate key on generated-ID table> 0 in 5 minPage
Lease conflict detected> 0Page
Worker space occupancy> 80% warn, > 95% pageWarn/Page
Clock regression > 1sany hostPage compute platform
Logical clock ahead > 5s for 5 minany instanceWarn
Segment remaining < 60s of consumptionany biz_tagPage

E.3 Control Plane vs Data Plane#

The data plane is next() — in-process, no network. The control plane is the lease service and the format registry. Control-plane outages must not affect the data plane for at least TTL − margin (20s); beyond that, they block new instances and fence existing ones. Alert on the first renewal error, not on fencing.

E.4 Debugging Suspected Duplicates#

  1. parse() the duplicate IDs → same worker_id, same ts? Worker collision. Different worker_id? Not a generator bug — look for re-inserts or replication conflicts.
  2. Look up lease history for that worker_id around ts — two holders?
  3. Same holder, same ts, same seq? Clock regression with trust-the-clock policy, or a restart without persisted last_ts.
Appendix F: Scale Evolution and Multi-Region

F.1 What Works at Each Scale#

ScaleApproachBreaks When
< 5K/s, one DBDB sequenceYou shard
5K–1M/s, sharded64-bit library + leases, or UUIDv7Worker space or languages multiply
1M–50M/s, multi-regionPer-region leases, partitioned worker space, registryAcquisitions, partner formats
Org-wideIdentity-lease platform, public-ID standard, alias service—

F.2 Multi-Region Path#

Diagram: F.2 Multi-Region Path

No per-ID cross-region traffic. A region partitioned forever still mints unique IDs. Global k-sorting degrades to cross-region skew.

F.3 What You Don't Build on Day One#

  • A network ID service
  • Region bits in use (reserve them; leave zero)
  • Dynamic segment step tuning
  • An alias service for foreign IDs
  • Keyed-permutation public IDs (start with random)
Appendix G: Multi-Tenancy, Privacy and Cost

G.1 What Each Format Leaks#

FormatCreation TimeGlobal VolumeTenant VolumeTopology
DB sequenceNoYesYes (via gaps)No
Snowflake-styleYes (ms)Partially (per-worker seq)PartiallyWorker/region
UUIDv7 / ULIDYes (ms)NoNoNo
UUIDv4 / random public IDNoNoNoNo
Per-tenant display numberNoNoOwn onlyNo

G.2 Per-Tenant Sequential Numbers#

When a customer wants "Invoice #1, #2, #3" per tenant: keep a counter row per tenant, increment in the same transaction as the insert. Throughput per tenant is limited to row-lock rate (~1–5K/s on a single row) — fine for invoices, wrong for events. These are display numbers, never keys.

G.3 Cost of Key Size#

extra_bytes = (16 − 8) × rows × (1 + secondary_indexes)          # clustered engines (InnoDB)
example     = 8 × 50e9 × 6 ≈ 2.4 TB of extra index

In heap-organized engines (PostgreSQL), secondary indexes point at tuple IDs rather than the PK, so the multiplier is smaller — but every foreign key column and FK index still doubles. Price per table; it's rarely worth optimizing outside the largest 5–10 tables.

  1. Loading the index…