Chapter 27: Distributed ID Generation

Why Distributed IDs

Auto-increment primary keys fail in distributed systems. When data is sharded across multiple databases, each shard generates its own sequence, causing ID collisions. Distributed ID generation solves this by creating globally unique identifiers without coordination.

Requirements

Requirement Why It Matters Trade-offs
Uniqueness No collisions across all nodes globally Coordination adds latency
Ordered Time-sortable for pagination, range queries, B-tree indexes Random IDs fragment indexes
Compact Small size (64-bit fits in a long, reduces storage/memory) 128-bit UUIDs are larger
Fast Generate millions per second without blocking Network calls for coordination slow generation
Coordination-free No central bottleneck or single point of failure Pure coordination-free approaches risk collisions
Interview Tip: When asked about distributed IDs, clarify requirements first. Does the system need ordering? Is 64-bit or 128-bit acceptable? Can you tolerate a coordination service? The answer drives your choice.

Why Auto-Increment Fails

graph LR A[App Server 1] --> B[DB Shard 1
ID: 1,2,3...] A --> C[DB Shard 2
ID: 1,2,3...] D[App Server 2] --> B D --> C style B fill:#ff6b6b,color:#1a1a2e style C fill:#ff6b6b,color:#1a1a2e

Both shards generate ID=1, causing collisions when aggregating data.

UUID (Universally Unique Identifier)

128-bit identifiers standardized in RFC 4122. Multiple versions with different generation strategies.

UUID Versions

Version Structure Pros Cons
v1 Timestamp + MAC address + counter Time-sortable, low collision risk Exposes MAC address (privacy risk), non-monotonic across nodes
v4 122 bits random + 6 bits version/variant No coordination, simple, privacy-safe Not sortable, fragments B-tree indexes, slower inserts
v6 Reordered v1 (time MSB-first) + MAC + counter Time-sortable, maintains v1 uniqueness Still exposes MAC, draft spec (not widely adopted)
v7 Unix timestamp (48-bit ms) + random (74 bits) Time-sortable, no MAC exposure, good for databases Draft spec, less adoption than v4

Example UUIDs

v1: 6ba7b810-9dad-11d1-80b4-00c04fd430c8
v4: 550e8400-e29b-41d4-a716-446655440000
v7: 017f22e2-79b0-7cc3-98c4-dc0c0c07398f

When to Use

Interview Insight: Mention that random UUIDs (v4) cause index fragmentation. Each insert is random, so B-trees rebalance frequently. Time-ordered IDs (v7, v1, v6) append to the index, much faster for high-throughput writes.

Twitter Snowflake

64-bit ID scheme designed by Twitter for high-volume distributed ID generation. Generates roughly time-sorted IDs without coordination.

Structure

graph LR A[Sign bit
1 bit
always 0] --> B[Timestamp
41 bits
ms since epoch] B --> C[Datacenter ID
5 bits
0-31] C --> D[Machine ID
5 bits
0-31] D --> E[Sequence
12 bits
0-4095]
Component Bits Purpose Range
Sign 1 Reserved (always 0 for positive long) 0
Timestamp 41 Milliseconds since custom epoch (e.g., 2010-11-04) ~69 years
Datacenter 5 Datacenter identifier 0-31 (32 datacenters)
Machine 5 Machine/server identifier within datacenter 0-31 (32 machines per DC)
Sequence 12 Per-machine counter (resets each millisecond) 0-4095 (4096 IDs/ms/machine)

Properties

Generation Algorithm

class SnowflakeIDGenerator {
    private long epoch = 1288834974657L;  // Custom epoch
    private long datacenterIdBits = 5L;
    private long machineIdBits = 5L;
    private long sequenceBits = 12L;

    private long datacenterId;
    private long machineId;
    private long sequence = 0L;
    private long lastTimestamp = -1L;

    public synchronized long nextId() {
        long timestamp = currentTimeMillis();

        // Clock moved backward - reject
        if (timestamp < lastTimestamp) {
            throw new RuntimeException("Clock moved backwards");
        }

        // Same millisecond - increment sequence
        if (timestamp == lastTimestamp) {
            sequence = (sequence + 1) & 4095; // Mask to 12 bits
            if (sequence == 0) {
                // Sequence overflow - wait for next ms
                timestamp = waitNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L; // New millisecond - reset
        }

        lastTimestamp = timestamp;

        return ((timestamp - epoch) << 22)
             | (datacenterId << 17)
             | (machineId << 12)
             | sequence;
    }
}
Interview Key Point: Snowflake IDs are roughly time-sorted, not perfectly sorted. Two machines generating IDs at the same millisecond will have the same timestamp prefix but different machine IDs, so sort order depends on the lower bits. Good enough for pagination and range scans.

Clock Skew Handling

If system clock moves backward (NTP correction), Snowflake rejects ID generation to prevent duplicate IDs. Alternative: wait until clock catches up, but adds latency.

ULID (Universally Unique Lexicographically Sortable Identifier)

128-bit identifier designed for time-sorting and readability. Encoded in Crockford Base32 (no ambiguous characters like 0/O, 1/I/l).

Structure

Component Bits Purpose Details
Timestamp 48 Unix timestamp in milliseconds ~8900 years (until year 10889)
Randomness 80 Cryptographically random bits 2^80 combinations per millisecond

Example

01ARZ3NDEKTSV4RRFFQ69G5FAV
|----------|-----------|
 Timestamp   Randomness
 (10 chars)  (16 chars)

ULID vs UUID v7

Feature ULID UUID v7
Size 128 bits 128 bits
Encoding Base32 (26 chars) Hex with dashes (36 chars)
Timestamp 48 bits (ms) 48 bits (ms)
Randomness 80 bits 74 bits (6 bits for version/variant)
Sortability Lexicographically sortable as string Binary sortable, string-sortable if unformatted
Standardization Community spec RFC draft (IETF)

When to Use

Interview Insight: ULID and UUID v7 solve the same problem. ULID is more compact as a string (26 vs 36 chars), but UUID v7 has better standardization momentum. Both are excellent for time-ordered distributed IDs.

Database Sequences

Traditional approach: database auto-increment. Works for single databases, fails in distributed systems. Solutions add complexity.

Single Database Auto-Increment

CREATE TABLE users (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(255)
);

Problem: Single point of failure, coordination bottleneck, doesn't scale horizontally.

Flickr's Ticket Server

Two MySQL databases configured with different auto-increment offsets generate non-overlapping IDs.

graph LR A[App Servers] --> B[Ticket DB 1
auto_increment_offset=1
auto_increment_increment=2
1,3,5,7...] A --> C[Ticket DB 2
auto_increment_offset=2
auto_increment_increment=2
2,4,6,8...]

Configuration

-- DB 1
SET @@auto_increment_offset = 1;
SET @@auto_increment_increment = 2;

-- DB 2
SET @@auto_increment_offset = 2;
SET @@auto_increment_increment = 2;

Pros and Cons

Pros Cons
Simple, uses existing DB features Doesn't scale beyond a few servers
Sequential IDs (good for ordering) Network round-trip for each ID (latency)
No custom code Single point of contention (each DB)
High availability (two DBs) Adding more servers requires reconfiguration

Pre-allocating ID Ranges

Optimization: allocate ranges (e.g., 1000 IDs at a time) to each app server, generate locally.

-- Server 1 claims 1-1000
-- Server 2 claims 1001-2000
-- Server 3 claims 2001-3000

Trade-off: Gaps in sequences if server crashes before using all IDs.

Interview Tip: Mention that database sequences are fine for moderate scale (< 10K writes/sec). Beyond that, coordination overhead kills throughput. Modern approach: generate IDs locally (Snowflake, ULID) without DB round-trip.

MongoDB ObjectID

12-byte (96-bit) identifier used as MongoDB's default _id. Combines timestamp, machine identifier, process ID, and counter.

Structure

graph LR A[Timestamp
4 bytes
Unix seconds] --> B[Machine ID
3 bytes
hostname hash] B --> C[Process ID
2 bytes] C --> D[Counter
3 bytes
random start]
Component Bytes Purpose Details
Timestamp 4 Unix timestamp in seconds Good until 2106
Machine ID 3 Hash of machine hostname Unique per machine
Process ID 2 Process ID of MongoDB instance Unique per process on machine
Counter 3 Incrementing counter Random initial value, 16M IDs/sec

Example

507f1f77bcf86cd799439011
|-------|-----|---|------|
Timestamp Machine PID Counter
(8 hex)  (6 hex) (4)  (6 hex)

Properties

Interview Note: ObjectID's 1-second timestamp is coarser than Snowflake (1ms) or ULID (1ms). Fine for MongoDB's use case (document creation rate), but Snowflake is better for high-volume event logging where millisecond ordering matters.

Instagram's Approach

Instagram generates 64-bit IDs using PostgreSQL schemas per shard. Embeds shard ID in the identifier for routing.

Structure

Component Bits Purpose
Timestamp 41 Milliseconds since custom epoch
Shard ID 13 Identifies which database shard (0-8191)
Sequence 10 Auto-increment per shard (0-1023)

PostgreSQL Schema

CREATE SCHEMA insta5; -- Shard 5

CREATE SEQUENCE insta5.table_seq;

CREATE OR REPLACE FUNCTION insta5.next_id(OUT result bigint) AS $$
DECLARE
    our_epoch bigint := 1314220021721;
    seq_id bigint;
    shard_id int := 5;
BEGIN
    SELECT nextval('insta5.table_seq') % 1024 INTO seq_id;
    result := (EXTRACT(EPOCH FROM clock_timestamp()) * 1000 - our_epoch) << 23;
    result := result | (shard_id << 10);
    result := result | (seq_id);
END;
$$ LANGUAGE PLPGSQL;

Advantages

Routing Example

def get_shard_id(id):
    return (id >> 10) & 8191  # Extract 13-bit shard ID

photo_id = 1234567890123456
shard = get_shard_id(photo_id)  # 5
# Query shard 5 directly
Interview Highlight: Instagram's approach embeds routing information (shard ID) in the ID itself. This eliminates a lookup table for "which shard has this record?" Trade-off: resharding is harder since shard ID is baked into IDs.

Clock Skew

Time-based ID generation relies on synchronized clocks. Clock drift or NTP corrections cause issues.

Problems

Issue Impact Example
Clock moves backward Duplicate IDs if timestamp repeats NTP correction subtracts 500ms
Clock drift IDs out of order across nodes Node A is 2 seconds ahead of Node B
Leap seconds Time jumps forward or repeats UTC leap second adjustment

Snowflake's Approach

if (timestamp < lastTimestamp) {
    // Clock moved backward - reject
    throw new RuntimeException(
        "Clock moved backwards. Refusing to generate ID"
    );
}

Trade-off: Availability hit (service errors until clock catches up) to prevent duplicate IDs.

Alternative: Wait for Clock

if (timestamp < lastTimestamp) {
    // Spin until clock catches up
    while (currentTimeMillis() <= lastTimestamp) {
        Thread.sleep(1);
    }
    timestamp = currentTimeMillis();
}

Trade-off: Adds latency (blocks ID generation).

Google Spanner's TrueTime

Uses GPS and atomic clocks to bound clock uncertainty. API returns time interval [earliest, latest].

TT.now() = [t_earliest, t_latest]
uncertainty = t_latest - t_earliest  // Typically < 10ms

Spanner waits out uncertainty before committing transactions to ensure external consistency.

Practical Mitigations

Interview Key Point: Clock skew is a real problem in distributed ID generation. Twitter Snowflake rejects generation on backward clock movement. Google Spanner uses atomic clocks to bound uncertainty. In interviews, mention NTP and show you understand the trade-off between availability (reject IDs) and correctness (prevent duplicates).

Comparison Matrix

Strategy Size Sortable Collision Risk Generation Speed Coordination Use Case
UUID v4 128-bit No Extremely low (2^122) Very fast (local) None Client-side, offline apps, no ordering needed
UUID v7 128-bit Yes (time) Extremely low Very fast (local) None Database PKs, time-ordered UUIDs
Snowflake 64-bit Yes (rough time) None (if config correct) Very fast (4M/sec/node) Machine ID config High-volume event IDs (Twitter, logs)
ULID 128-bit Yes (time) Extremely low (2^80) Very fast (local) None APIs, user-facing IDs, string sorting
DB Sequence 64-bit Yes (strictly) None (DB guarantees) Moderate (network) Database Small-medium scale, existing DB infra
MongoDB ObjectID 96-bit Yes (rough time, 1s) None (if config correct) Very fast (local) Machine config MongoDB documents
Instagram 64-bit Yes (time) None (DB sequence) Fast (DB call) PostgreSQL per shard Sharded PostgreSQL, routing in ID

Performance Comparison

Strategy IDs/sec (single node) Latency Bottleneck
UUID v4/v7 10M+ < 1μs CPU/RNG
Snowflake 4M < 1μs Sequence overflow (4096/ms)
ULID 10M+ < 1μs CPU/RNG
DB Sequence 10K-100K 1-10ms Network + DB lock
MongoDB ObjectID 10M+ < 1μs Counter overflow
Interview Insight: In real interviews, you'll be asked to compare strategies. Key dimensions: size (64 vs 128-bit), sortability (pagination?), coordination (bottleneck?), and generation speed (millions/sec local vs thousands/sec with DB call).

Choosing the Right Strategy

graph TD A[Need Distributed IDs] --> B{Need ordering?} B -->|No| C[UUID v4
Simple, fast, no coordination] B -->|Yes| D{What size?} D -->|128-bit OK| E{String or binary?} E -->|String in APIs| F[ULID
Base32, readable] E -->|Binary in DB| G[UUID v7
Standard, time-sorted] D -->|Need 64-bit| H{Have coordination service?} H -->|No| I[Snowflake
Requires machine ID config] H -->|Yes, using PostgreSQL| J[Instagram approach
Shard ID in ID] H -->|Yes, using MongoDB| K[ObjectID
Built-in] H -->|Legacy DB| L{Can you scale it?} L -->|Yes| M[DB Sequence
Flickr ticket server] L -->|No| N[Migrate to Snowflake/ULID]

Decision Framework

Requirement Recommendation Why
Client-side generation (offline apps) UUID v4 No server dependency
Database primary keys (high write volume) UUID v7 or ULID Time-ordered reduces index fragmentation
Event logging (millions/sec) Snowflake Compact 64-bit, time-sorted, proven at scale
User-facing IDs in URLs/APIs ULID Readable, unambiguous, sortable as string
Sharded DB with routing in ID Instagram approach Shard ID embedded for direct routing
Small-medium scale (< 10K writes/sec) DB Sequence Simple, uses existing infrastructure
MongoDB ObjectID Built-in, optimized for Mongo

Anti-Patterns

Migration Strategy

Migrating from auto-increment to distributed IDs:

  1. Change ID column type (INT → BIGINT for Snowflake, CHAR(26) for ULID)
  2. Dual-write: generate both old and new IDs during transition
  3. Update foreign keys to reference new IDs
  4. Backfill old records with new IDs (or keep old IDs as legacy)
  5. Switch reads to new IDs
  6. Drop old ID column
Interview Framework: When asked "how would you generate IDs for system X?", follow this script:
  1. Clarify requirements: ordering? size? scale? coordination available?
  2. Propose 2-3 options with trade-offs
  3. Recommend one based on requirements
  4. Discuss clock skew, collision risk, and failure modes
Show you understand trade-offs, not just memorized one solution.

Summary