system-design46 min read

How to Design a Unique ID Generation System — A Senior+ Guide | Ayodhyya

How to Design a Unique ID Generation System

A Senior+ Guide — Building Snowflake, KUID, ULID, and Distributed ID Generators for Globally Unique, Time-Ordered Identifiers at Billion-QPS Scale

Published: April 12, 2024 Reading Time: ~30 min By Ayodhyya

1. Introduction — Why Unique IDs Matter

Every distributed system on the planet shares one fundamental challenge: generating identifiers that are guaranteed to be unique across thousands of machines, data centers, and geographic regions — all without becoming a bottleneck. From database primary keys to event identifiers in Kafka, from session tokens to IoT device telemetry, unique IDs are the invisible scaffolding that holds modern software together.

The requirements for a well-designed unique ID system are deceptively complex. The IDs must be globally unique — two independently operating services must never produce the same value. They should ideally be time-ordered, meaning IDs generated later are lexicographically greater than those generated earlier, which enables efficient database indexing. They must be generated at massive scale — systems like Twitter, Uber, and Instagram generate millions of IDs per second. And they must work in distributed environments where no central coordination is available.

Consider the bit-width trade-off. A 64-bit integer gives you roughly 1.8 × 10¹⁹ possible values — sounds enormous until you realize that at 10 million IDs per second, you exhaust the space in about 58 years. A 128-bit UUID gives you 3.4 × 10³⁸ values, which is effectively infinite, but at the cost of larger storage, slower indexing, and reduced cache efficiency. The choice between 64-bit and 128-bit IDs is not merely technical; it has profound implications for database performance, network bandwidth, and storage costs at scale.

Distributed generation introduces its own class of problems. If every machine generates IDs independently, how do you prevent collisions? If IDs must be monotonically increasing, how do you coordinate across machines that may have clock skew of tens of milliseconds? These questions have spawned a rich ecosystem of ID generation strategies — from Twitter's Snowflake to Google's TrueTime-powered Spanner IDs, from the elegant simplicity of ULID to the hash-based collision resistance of KUID.

This article provides a comprehensive, senior-level deep dive into unique ID generation. We will dissect the bit layouts of every major ID format, build a production-ready ID generator service in C#, analyze the failure modes and trade-offs of each approach, and equip you with the knowledge to design an ID system that meets the demands of billion-QPS workloads.

Who This Guide Is For: Senior engineers preparing for system design interviews, architects designing distributed systems, and tech leads evaluating ID generation strategies for production systems. We assume familiarity with distributed systems fundamentals, binary arithmetic, and database internals.

2. Functional & Non-Functional Requirements

Before choosing an ID generation strategy, we must clearly articulate what we need. A requirements-driven approach prevents over-engineering and ensures the chosen solution fits the actual workload.

Functional Requirements

RequirementDescriptionPriority
Globally UniqueNo two calls across any machine, region, or time window should produce the same IDP0 — Critical
Time-OrderedIDs generated later should sort after IDs generated earlier (lexicographic ordering)P0 — Critical
64-bit or 128-bitIDs must fit in a 64-bit integer for efficient database storage, or 128-bit for maximum uniquenessP0 — Critical
Monotonically IncreasingEach new ID from the same generator must be strictly greater than the previous oneP1 — Important
PortableIDs must be usable across different databases, message queues, and storage systemsP1 — Important

Non-Functional Requirements

RequirementTargetRationale
Latency< 1ms per ID generationID generation sits on the hot path of every write operation
Throughput≥ 10 million IDs/second per regionSupports large-scale systems like social media feeds
Availability99.999% (five nines)ID generation failure blocks all writes — it must never go down
Partition ToleranceContinue generating IDs even during network partitionsDistributed systems must handle network failures gracefully
No Single Point of FailureMultiple independent generators can operate simultaneouslyCentralized generators are inherently fragile
graph TD A[ID Generation Requirements] --> B[Uniqueness] A --> C[Performance] A --> D[Reliability] A --> E[Ordering] B --> B1[Globally unique across all nodes] B --> B2[No coordination required] B --> B3[Collision probability < 10⁻¹⁸] C --> C1[Sub-millisecond latency] C --> C2[10M+ IDs/second] C --> C3[Minimal CPU/memory overhead] D --> D1[99.999% availability] D --> D2[No single point of failure] D --> D3[Fault-tolerant to node crashes] E --> E1[Time-ordered/lexicographic] E --> E2[Monotonically increasing] E --> E3[Database index friendly]

3. Capacity Estimation

Capacity planning for ID generation requires precise arithmetic over bit widths, generation rates, and time horizons. Getting this wrong leads to either wasted bits (over-provisioning) or catastrophic collisions (under-provisioning).

Bits Allocation Analysis

64-bit ID space = 2⁶⁴ = 18,446,744,073,709,551,616 possible values ≈ 1.8 × 10¹⁹

At 10 million IDs per second, a 64-bit space lasts approximately:

1.8 × 10¹⁹ ÷ 10⁷ = 1.8 × 10¹² seconds ≈ 57,000 years

This seems comfortable, but the practical limit is determined by how many bits are allocated to each component. In a Snowflake-style 64-bit ID with 41 bits for timestamp, 10 bits for machine, and 12 bits for sequence, the maximum duration is:

2⁴¹ milliseconds = 2,199,023,255,552 ms ≈ 69.7 years from epoch

And the per-millisecond throughput per machine is 2¹² = 4,096 IDs. With 1,024 machines (10 bits), the total throughput is 4,096 × 1,024 = 4,194,304 IDs per millisecond, or roughly 4.2 billion IDs per second.

Throughput Requirements

Scale TierIDs/SecondIDs/DayBits Needed (64-bit)Example System
Small1,00086.4M41 (timestamp) + 0 + 10 (sequence)Internal tool
Medium100,0008.64B41 + 5 (machine) + 12SaaS application
Large10,000,000864B41 + 10 (machine) + 12Social media platform
Massive1,000,000,00086.4T41 + 20 (machine) + 2 (sequence)Global IoT mesh

Storage Estimation

If we store 1 billion IDs per day in a database with 64-bit primary keys, the storage overhead per row for the ID alone is 8 bytes. For a year of data:

365 × 10⁹ rows × 8 bytes = 2.92 TB/year (ID column only)

Switching to 128-bit UUIDs doubles this to 5.84 TB/year — a significant storage and indexing cost that justifies careful ID format selection. When we factor in B-tree index overhead (typically 2-3× the raw data size), 64-bit IDs offer a compelling advantage: approximately 6-9 TB of index storage versus 12-18 TB for 128-bit UUIDs.

4. ID Format Design

The canonical approach to designing a structured unique ID is the bit-packing layout, where we carve the ID into distinct fields, each serving a specific purpose. The most common fields are timestamp, machine/node identifier, and sequence number.

graph LR subgraph "64-bit ID Layout" S["Sign
1 bit"] --> T["Timestamp
41 bits"] --> DC["Datacenter
5 bits"] --> M["Machine
5 bits"] --> SEQ["Sequence
12 bits"] end style S fill:#fee2e2,stroke:#ef4444,color:#000 style T fill:#dbeafe,stroke:#0088ff,color:#000 style DC fill:#d1fae5,stroke:#10b981,color:#000 style M fill:#fef3c7,stroke:#f59e0b,color:#000 style SEQ fill:#e0e7ff,stroke:#6366f1,color:#000

Component Breakdown

Timestamp (41 bits): Stores milliseconds since a custom epoch. With 41 bits, we can represent 2⁴¹ ms ≈ 69.7 years. If we set our epoch to January 1, 2025, the IDs will be valid until approximately 2094. The timestamp provides two critical properties: time-orderedness and the ability to extract the creation time from any ID.

Datacenter ID (5 bits): Supports up to 32 data centers. In practice, most organizations use fewer than 10, leaving room for growth. This field prevents collisions between machines in different data centers that might otherwise have the same machine ID.

Machine ID (5 bits): Supports up to 32 machines per data center. Combined with the datacenter field, this provides 32 × 32 = 1,024 unique machine identifiers. For larger deployments, we can rebalance the bit allocation — for instance, 6 bits for datacenter and 4 bits for machine, or vice versa.

Sequence Number (12 bits): A per-machine counter that increments for each ID generated within the same millisecond. With 12 bits, each machine can generate 4,096 IDs per millisecond, or approximately 4.2 million IDs per second — more than sufficient for most use cases.

⚠️ Clock Reset Problem: If the system clock moves backward (due to NTP correction, manual adjustment, or VM migration), the timestamp component decreases, potentially producing IDs that collide with previously generated IDs. Production systems must handle this by either rejecting clock adjustments, using a persisted "last timestamp" value, or switching to an epoch-relative approach with monotonic timestamp tracking.

5. API Design

A well-designed ID generation API exposes three core operations: generating a single ID, generating a batch of IDs, and inspecting the next ID without consuming it.

Core API Methods

MethodSignatureDescriptionUse Case
next_id()long next_id()Generates and returns the next unique IDStandard single-record insertion
batch_ids(count)long[] batch_ids(int count)Generates a contiguous range of IDsBulk inserts, batch processing
peek()long peek()Returns the next ID without incrementing the counterPre-flight checks, distributed coordination

REST API Endpoints

POST /api/v1/ids/generate
{
    "count": 1,
    "format": "decimal"   // "decimal" | "hex" | "base62" | "base32"
}

Response:
{
    "ids": ["5847291038475920384"],
    "metadata": {
        "generator_id": "gen-us-east-01",
        "timestamp": 1752508800000,
        "sequence": 1
    }
}

GET /api/v1/ids/peek
Response:
{
    "next_id": "5847291038475920385",
    "remaining_capacity_in_window": 4095
}

POST /api/v1/ids/validate
{
    "id": "5847291038475920384"
}
Response:
{
    "valid": true,
    "timestamp": "2026-07-14T10:30:00.000Z",
    "machine_id": 7,
    "datacenter_id": 3,
    "sequence": 0
}

6. High-Level Architecture

The architecture of a production ID generation system must balance three competing concerns: high availability (ID generation must never be a bottleneck), uniqueness (no two nodes should produce the same ID), and performance (generation must be sub-millisecond).

graph TB subgraph "Application Layer" App1[Service A] App2[Service B] App3[Service C] end subgraph "ID Generation Layer" LB[Load Balancer] Gen1[Generator Node 1
Datacenter: US-East
Machine ID: 1] Gen2[Generator Node 2
Datacenter: US-East
Machine ID: 2] Gen3[Generator Node 3
Datacenter: EU-West
Machine ID: 1] Gen4[Generator Node 4
Datacenter: AP-South
Machine ID: 1] end subgraph "Coordination Layer" ZK[ZooKeeper / etcd
Machine ID Assignment] NTP[NTP Servers
Clock Synchronization] Config[Config Service
Epoch, Bit Layout] end subgraph "Storage Layer" DB1[(Primary DB)] DB2[(Replica DB)] Cache[Redis Cache
Segment Pre-allocation] end App1 --> LB App2 --> LB App3 --> LB LB --> Gen1 LB --> Gen2 LB --> Gen3 LB --> Gen4 Gen1 --> ZK Gen2 --> ZK Gen3 --> ZK Gen4 --> ZK Gen1 --> NTP Gen2 --> NTP Gen3 --> NTP Gen4 --> NTP Gen1 --> Config Gen2 --> Config Gen3 --> Config Gen4 --> Config Gen1 --> Cache Gen2 --> Cache Gen1 --> DB1 DB1 --> DB2

In this architecture, each ID generator node is an independent, stateless service that derives its uniqueness from the combination of its machine ID (assigned via ZooKeeper or etcd) and its local sequence counter. The load balancer distributes ID generation requests across available nodes, and the NTP servers ensure clock synchronization to within a few milliseconds.

sequenceDiagram participant App as Application participant LB as Load Balancer participant Gen as Generator Node participant ZK as ZooKeeper participant NTP as NTP Server Note over Gen: Startup Phase Gen->>ZK: Register and obtain machine_id ZK-->>Gen: machine_id = 7 Gen->>NTP: Synchronize clock NTP-->>Gen: offset = +2ms Gen->>Gen: Initialize sequence = 0 Gen->>Gen: last_timestamp = now() Note over App,Gen: Runtime Phase App->>LB: next_id() LB->>Gen: Route to node Gen->>Gen: current_ms = now() alt Same millisecond as last_id Gen->>Gen: sequence++ alt sequence > 4095 Gen->>Gen: Wait for next ms end else New millisecond Gen->>Gen: sequence = 0 Gen->>Gen: last_timestamp = current_ms end Gen-->>App: Return 64-bit ID

7. UUID v4 — Random Generation

UUID v4 is the simplest ID generation strategy: generate 122 random bits (out of 128 total, with 6 bits reserved for version and variant). It requires zero coordination between nodes, making it the default choice for many distributed systems.

UUID v4 Bit Layout

graph LR subgraph "UUID v4 (128 bits)" V["Version
4 bits
0100"] --> R1["Random
62 bits"] --> V2["Variant
2 bits
10"] --> R2["Random
60 bits"] end style V fill:#fee2e2,stroke:#ef4444,color:#000 style R1 fill:#dbeafe,stroke:#0088ff,color:#000 style V2 fill:#d1fae5,stroke:#10b981,color:#000 style R2 fill:#dbeafe,stroke:#0088ff,color:#000

Collision Probability

The probability of a collision in UUID v4 follows the birthday problem. For n UUIDs generated from a 122-bit random space:

P(collision) ≈ n² / (2 × 2¹²²) = n² / (2 × 5.3 × 10³⁶)

At 1 billion UUIDs per year, the collision probability is approximately 10⁻²⁰ — essentially zero for any practical system. Even generating a UUID every nanosecond for the entire age of the universe would not produce a meaningful collision risk.

✅ When to Use UUID v4: When you need zero-coordination unique IDs, when ordering is not important, when you have sufficient storage for 128-bit values, and when simplicity is paramount. Common use cases include session tokens, request tracing IDs, and document identifiers in NoSQL databases.
❌ When to Avoid UUID v4: When you need time-ordered IDs (UUID v4 is completely random), when storage is constrained (128 bits vs 64 bits), when database index performance matters (random 128-bit values cause B-tree page splits), and when you need to extract temporal information from the ID.

C# Implementation

public static class UUIDv4Generator
{
    private static readonly RandomNumberGenerator Rng = RandomNumberGenerator.Create();

    public static Guid Generate()
    {
        byte[] bytes = new byte[16];
        Rng.GetBytes(bytes);

        // Set version bits (4 = UUID v4)
        bytes[6] = (byte)((bytes[6] & 0x0F) | 0x40);

        // Set variant bits (RFC 4122)
        bytes[8] = (byte)((bytes[8] & 0x3F) | 0x80);

        return new Guid(bytes);
    }

    public static string GenerateFormatted()
    {
        return Generate().ToString("D"); // e.g., "550e8400-e29b-41d4-a716-446655440000"
    }
}

8. UUID v7 — Time-Ordered

UUID v7 (defined in RFC 9562, published May 2024) solves UUID v4's biggest limitation: it prefixes the UUID with a Unix timestamp in milliseconds, making UUIDs lexicographically sortable while retaining 128-bit collision resistance.

UUID v7 Bit Layout

graph LR subgraph "UUID v7 (128 bits)" TS["Unix Timestamp
48 bits
milliseconds"] --> V["Version
4 bits
0111"] --> R["Random
62 bits"] --> VAR["Variant
2 bits
10"] end style TS fill:#dbeafe,stroke:#0088ff,color:#000 style V fill:#d1fae5,stroke:#10b981,color:#000 style R fill:#fef3c7,stroke:#f59e0b,color:#000 style VAR fill:#e0e7ff,stroke:#6366f1,color:#000

The 48-bit millisecond timestamp provides 2⁴⁸ ms ≈ 8.9 years of unique time representation from the Unix epoch. After that, the timestamp wraps around, but the 62 random bits provide sufficient collision protection even across timestamp wraps.

UUID v7 vs UUID v4 Comparison

PropertyUUID v4UUID v7
Bit Width128 bits128 bits
Time-OrderedNoYes
Random Bits122 bits62 bits
SortableNoYes (lexicographic)
Database Index FriendlyNo (random inserts)Yes (append-mostly)
Extractable TimestampNoYes (48-bit ms precision)
Collision Risk (1B/year)~10⁻²⁰~10⁻¹⁰ (still negligible)
StandardRFC 4122RFC 9562
💡 Key Insight: UUID v7 is the best default choice for new systems that need 128-bit IDs. It provides the collision resistance of UUID v4 with the time-ordering properties of Snowflake, all without requiring any coordination between generator nodes. The 62 random bits are more than sufficient to prevent collisions in practice.

9. Twitter Snowflake Deep Dive

Twitter's Snowflake, open-sourced in 2014, remains the gold standard for 64-bit distributed ID generation. Its elegant bit-packing layout produces IDs that are time-ordered, compact, and generated without coordination between nodes.

Snowflake 64-bit Layout

graph TB subgraph "Snowflake 64-bit ID" direction LR A["Sign Bit
1 bit
Always 0"] --- B["Timestamp
41 bits
ms since epoch"] --- C["Datacenter
5 bits
0-31"] --- D["Worker
5 bits
0-31"] --- E["Sequence
12 bits
0-4095"] end style A fill:#fee2e2,stroke:#ef4444,color:#000 style B fill:#dbeafe,stroke:#0088ff,color:#000 style C fill:#d1fae5,stroke:#10b981,color:#000 style D fill:#fef3c7,stroke:#f59e0b,color:#000 style E fill:#e0e7ff,stroke:#6366f1,color:#000

Bit Allocation Breakdown

FieldBitsRangePurpose
Sign10 (always positive)Ensures the ID is a positive 64-bit integer
Timestamp410 to 2,199,023,255,551Milliseconds since custom epoch (~69.7 years)
Datacenter ID50 to 31Identifies the data center (up to 32 DCs)
Worker ID50 to 31Identifies the machine within a data center (up to 32)
Sequence120 to 4,095Per-ms counter for IDs within the same millisecond

Core Algorithm

public class SnowflakeGenerator
{
    // Custom epoch: January 1, 2025 00:00:00 UTC
    private const long Epoch = 1735689600000L;

    // Bit positions
    private const int TimestampLeftShift = 22;   // 5 + 5 + 12 = 22
    private const int DatacenterLeftShift = 17;  // 5 + 12 = 17
    private const int WorkerLeftShift = 12;       // 12

    // Masks
    private const long TimestampMask = 0x1FFFFFFFFFFL;  // 41 bits
    private const long DatacenterMask = 0x1FL;           // 5 bits
    private const long WorkerMask = 0x1FL;               // 5 bits
    private const long SequenceMask = 0xFFFL;            // 12 bits

    private readonly long _datacenterId;
    private readonly long _workerId;
    private long _sequence = 0L;
    private long _lastTimestamp = -1L;
    private readonly object _lock = new object();

    public SnowflakeGenerator(long datacenterId, long workerId)
    {
        if (datacenterId < 0 || datacenterId > DatacenterMask)
            throw new ArgumentException($"Datacenter ID must be 0-{DatacenterMask}");
        if (workerId < 0 || workerId > WorkerMask)
            throw new ArgumentException($"Worker ID must be 0-{WorkerMask}");

        _datacenterId = datacenterId;
        _workerId = workerId;
    }

    public long NextId()
    {
        lock (_lock)
        {
            long timestamp = GetCurrentTimestamp();

            if (timestamp < _lastTimestamp)
            {
                throw new Exception(
                    $"Clock moved backwards! Refusing to generate ID for " +
                    $"{_lastTimestamp - timestamp} milliseconds.");
            }

            if (timestamp == _lastTimestamp)
            {
                _sequence = (_sequence + 1) & SequenceMask;
                if (_sequence == 0)
                {
                    // Sequence exhausted in this ms, wait for next ms
                    timestamp = WaitNextMillis(_lastTimestamp);
                }
            }
            else
            {
                _sequence = 0L;
            }

            _lastTimestamp = timestamp;

            return ((timestamp - Epoch) << TimestampLeftShift) |
                   (_datacenterId << DatacenterLeftShift) |
                   (_workerId << WorkerLeftShift) |
                   _sequence;
        }
    }

    private long GetCurrentTimestamp()
    {
        return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
    }

    private long WaitNextMillis(long lastTimestamp)
    {
        long timestamp = GetCurrentTimestamp();
        while (timestamp <= lastTimestamp)
        {
            timestamp = GetCurrentTimestamp();
        }
        return timestamp;
    }

    public (long timestamp, long datacenter, long worker, long sequence)
        Decompose(long id)
    {
        long timestamp = ((id >> TimestampLeftShift) & TimestampMask) + Epoch;
        long datacenter = (id >> DatacenterLeftShift) & DatacenterMask;
        long worker = (id >> WorkerLeftShift) & WorkerMask;
        long sequence = id & SequenceMask;
        return (timestamp, datacenter, worker, sequence);
    }
}

Snowflake Failure Modes

Clock Backward: If NTP adjusts the clock backward, the timestamp component decreases, potentially generating IDs that overlap with recently generated ones. Mitigation: persist the last timestamp to disk and refuse to generate IDs until the clock catches up.
Worker ID Exhaustion: With only 5 bits for worker ID, a single data center supports at most 32 machines. If a data center grows beyond 32 machines, you must either rebalance the bit allocation (sacrificing datacenter bits) or switch to a different strategy.
Sequence Overflow: If a single machine generates more than 4,096 IDs within a single millisecond, the sequence overflows. The standard approach is to spin-wait until the next millisecond, but this can cause latency spikes under extreme load.

10. ULID — Universally Unique Lexicographically Sortable

ULID (Universally Unique Lexicographically Sortable Identifier) is a 128-bit ID format that combines a 48-bit millisecond timestamp with 80 bits of cryptographically random data, encoded in Crockford's Base32 alphabet. ULIDs sort lexicographically in the same order as their timestamps.

ULID Structure

graph LR subgraph "ULID (26 chars, 128 bits)" TS["Timestamp
48 bits
10 chars"] --- R["Randomness
80 bits
16 chars"] end style TS fill:#dbeafe,stroke:#0088ff,color:#000 style R fill:#fef3c7,stroke:#f59e0b,color:#000

A ULID string looks like: 01ARZ3NDEKTSV4RRFFQ69G5FAV

  • First 10 characters: Crockford Base32 encoded 48-bit millisecond timestamp
  • Last 16 characters: Crockford Base32 encoded 80-bit random value

Crockford's Base32 Alphabet

ULID uses Crockford's Base32, which excludes ambiguous characters (I, L, O, U) and is case-insensitive. The alphabet is: 0123456789ABCDEFGHJKMNPQRSTVWXYZ

✅ ULID Advantages: Lexicographic sorting, URL-safe strings, case-insensitive, no special characters, 48-bit timestamp precision, 80-bit randomness (collision-resistant), and no coordination required.

C# ULID Implementation

public class ULIDGenerator
{
    private static readonly char[] CrockfordAlphabet =
        "0123456789ABCDEFGHJKMNPQRSTVWXYZ".ToCharArray();

    private static readonly RandomNumberGenerator Rng =
        RandomNumberGenerator.Create();

    private static readonly object Lock = new object();
    private static long _lastTimestamp;
    private static int _lastRandomBits;

    public static string Generate()
    {
        byte[] randomness = new byte[10];
        int randomValue;

        lock (Lock)
        {
            long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

            Rng.GetBytes(randomness);
            randomValue = BitConverter.ToInt32(randomness, 0) & 0x7FFFFFFF;

            // Ensure monotonicity within the same millisecond
            if (timestamp == _lastTimestamp)
            {
                randomValue = (_lastRandomBits + 1) & 0x7FFFFFFF;
                if (randomValue == 0)
                {
                    // Overflow: wait for next millisecond
                    while (timestamp <= _lastTimestamp)
                    {
                        timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
                    }
                    Rng.GetBytes(randomness);
                    randomValue = BitConverter.ToInt32(randomness, 0) & 0x7FFFFFFF;
                }
            }

            _lastTimestamp = timestamp;
            _lastRandomBits = randomValue;
        }

        char[] result = new char[26];
        EncodeTimestamp(_lastTimestamp, result, 0);
        EncodeRandom(randomness, result, 10);

        return new string(result);
    }

    private static void EncodeTimestamp(long timestamp, char[] result, int offset)
    {
        for (int i = 9; i >= 0; i--)
        {
            result[offset + i] = CrockfordAlphabet[timestamp & 0x1F];
            timestamp >>= 5;
        }
    }

    private static void EncodeRandom(byte[] randomness, char[] result, int offset)
    {
        // Encode 80 bits of randomness into 16 Base32 characters
        ulong high = BitConverter.ToUInt64(randomness, 0);
        uint low = BitConverter.ToUInt32(randomness, 8);

        for (int i = 15; i >= 0; i--)
        {
            int index;
            if (i >= 12)
                index = (int)(low & 0x1F);
            else
                index = (int)(high & 0x1F);

            result[offset + i] = CrockfordAlphabet[index];

            if (i > 7)
                low >>= 5;
            else
                high >>= 5;
        }
    }

    public static (DateTime timestamp, byte[] randomness) Parse(string ulid)
    {
        if (ulid.Length != 26)
            throw new ArgumentException("ULID must be exactly 26 characters");

        long timestamp = 0;
        for (int i = 0; i < 10; i++)
        {
            timestamp = (timestamp << 5) | DecodeChar(ulid[i]);
        }

        return ( DateTimeOffset.FromUnixTimeMilliseconds(timestamp).UtcDateTime,
                 null ); // Simplified; full implementation would decode all 16 chars
    }

    private static int DecodeChar(char c)
    {
        if (c >= '0' && c <= '9') return c - '0';
        if (c >= 'A' && c <= 'Z') return c - 'A' + 10;
        if (c >= 'a' && c <= 'z') return c - 'a' + 10;
        throw new ArgumentException($"Invalid Crockford Base32 character: {c}");
    }
}

11. KUID — Hash-Based Unique IDs

KUID (Krea's Unique ID) represents a different philosophical approach to ID generation. Rather than encoding temporal or positional information into the ID, KUID generates identifiers by hashing input data (such as content hashes, user IDs, or timestamps combined with random salts) to produce collision-resistant identifiers.

KUID Design Principles

graph TD A[Input Data] --> B[Hash Function
SHA-256 / BLAKE3] B --> C[256-bit Hash Output] C --> D[Truncation
128-bit / 64-bit] D --> E[KUID] F[Random Salt] --> B G[Timestamp] --> B style A fill:#dbeafe,stroke:#0088ff,color:#000 style B fill:#fef3c7,stroke:#f59e0b,color:#000 style C fill:#d1fae5,stroke:#10b981,color:#000 style D fill:#e0e7ff,stroke:#6366f1,color:#000 style E fill:#fee2e2,stroke:#ef4444,color:#000

The key property of KUID is deterministic collision resistance: given the same input, the same ID is always produced, which enables idempotent operations. If the input data changes even slightly, the hash output changes completely (avalanche effect), ensuring that different inputs produce different IDs.

KUID Properties

PropertyValueImplication
Bit Width128 or 256 bitsConfigurable based on collision tolerance
Time-OrderedNo (by default)Not suitable for append-mostly indexes
DeterministicYesSame input → same ID (idempotent)
Collision Resistance2⁶⁴ work factor (128-bit)Computationally infeasible to find collisions
Coordination RequiredNoneFully distributed

When to Use KUID

  • Content-addressed storage: When the ID should represent the content itself (like IPFS CIDs)
  • Idempotent operations: When retrying the same request must produce the same ID
  • Deduplication: When you need to detect duplicate entries based on content
  • Event sourcing: When event IDs should be derived from event content
⚠️ KUID Limitation: KUIDs are not time-ordered, which means they cause random inserts in B-tree indexes. For write-heavy workloads, this leads to page splits and degraded insert performance. Consider combining KUID with a separate sort key (like a timestamp column) for optimal database performance.

12. Database Auto-Increment & Multi-Master

The simplest ID generation strategy is the database's built-in auto-increment feature. A single database instance maintains a counter and atomically increments it for each new row. This approach is elegant but fundamentally limited to a single node.

Single-Node Auto-Increment

CREATE TABLE orders (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    customer_id BIGINT NOT NULL,
    amount DECIMAL(10, 2),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- The database guarantees:
-- First insert: id = 1
-- Second insert: id = 2
-- Always sequential, always unique (within this table)

Multi-Master Replication Challenges

graph TB subgraph "Multi-Master Setup" DB1[Master 1
AUTO_INCREMENT = 1
INCREMENT BY 1] DB2[Master 2
AUTO_INCREMENT = 2
INCREMENT BY 2] DB3[Master 3
AUTO_INCREMENT = 3
INCREMENT BY 3] end subgraph "Generated IDs" ID1[1, 4, 7, 10, 13...] ID2[2, 5, 8, 11, 14...] ID3[3, 6, 9, 12, 15...] end DB1 --> ID1 DB2 --> ID2 DB3 --> ID3 Note["⚠️ IDs are unique but NOT sequential!"]

In a multi-master setup, we typically assign each master a different starting value and increment step. For example, with 3 masters: Master 1 starts at 1 and increments by 3, Master 2 starts at 2 and increments by 3, Master 3 starts at 3 and increments by 3. This guarantees uniqueness but destroys sequential ordering.

Gap Issues

Even with a single master, gaps in auto-increment values are common due to:

  • Transaction rollbacks: An ID is allocated but the transaction fails, leaving a gap
  • Batch inserts: The database pre-allocates a range of IDs, and if the batch is partially rejected, gaps remain
  • Server restarts: Many databases cache the next ID in memory and jump ahead on restart
  • Replication lag: Slave servers may allocate IDs that conflict with the master during failover
💡 Key Insight: Database auto-increment works perfectly for single-node systems with moderate write volume. It becomes problematic when you need horizontal scaling, multi-data-center deployment, or zero-downtime failover. For these scenarios, consider dedicated ID generation services or application-level strategies like Snowflake.

13. Clock Synchronization & NTP

Clock synchronization is the Achilles' heel of timestamp-based ID generation. Network Time Protocol (NTP) typically synchronizes clocks to within 1-50 milliseconds, but even this small drift can cause ID collisions or ordering violations.

Clock Drift Scenarios

graph LR subgraph "Scenario 1: Clock Jump Forward" A1[Time T] --> A2[Time T+100ms
NTP correction] A2 --> A3[IDs in gap
T+1 to T+99] end subgraph "Scenario 2: Clock Jump Backward" B1[Time T+50] --> B2[Time T+30
NTP correction] B2 --> B3[Potential
ID collision!] end style B3 fill:#fee2e2,stroke:#ef4444,color:#000

Clock jump forward: When NTP corrects a slow clock forward, IDs generated during the skipped interval are "lost" — they were never assigned. This is generally harmless but creates gaps in the ID sequence.

Clock jump backward: When NTP corrects a fast clock backward, the system may generate IDs with timestamps smaller than previously generated IDs, creating potential collisions with the Snowflake algorithm (since the same timestamp + datacenter + worker + sequence could repeat).

Google TrueTime

Google's Spanner database uses TrueTime, a globally synchronized clock that provides not just a time value but an uncertainty interval. TrueTime returns [earliest, latest] such that the actual time is guaranteed to lie within that interval. Spanner then waits out the uncertainty before committing transactions, ensuring external consistency.

TrueTime.now() = [earliest, latest] where uncertainty ≈ 1-7 ms

TrueTime is built on GPS receivers and atomic clocks in each data center, providing significantly better accuracy than NTP alone. Most organizations cannot replicate this infrastructure, making clock synchronization a practical challenge for Snowflake-style ID generators.

Leap Second Considerations

Leap seconds are occasional adjustments to UTC to account for the slowing of Earth's rotation. When a leap second occurs, the Unix timestamp may repeat or skip a second, depending on how the system handles it. Linux systems typically implement a "leap smear," gradually adjusting the clock over several hours to avoid a discontinuity. This is important for ID generators because a repeated second could produce duplicate timestamps.

14. Segment-Based Allocation

Segment-based ID allocation is a hybrid approach that combines the simplicity of database auto-increment with the performance of in-memory generation. A central service pre-allocates ID segments (ranges) from the database, and application servers generate IDs from their local segment without any further coordination.

sequenceDiagram participant App1 as App Server 1 participant App2 as App Server 2 participant Seg as Segment Service participant DB as Database Note over Seg,DB: Initial Setup App1->>Seg: Request ID Segment Seg->>DB: UPDATE id_allocations SET max_id = max_id + 1000 DB-->>Seg: max_id = 1000 Seg-->>App1: Segment [1, 1000] App2->>Seg: Request ID Segment Seg->>DB: UPDATE id_allocations SET max_id = max_id + 1000 DB-->>Seg: max_id = 2000 Seg-->>App2: Segment [1001, 2000] Note over App1: Generate IDs locally loop ID Generation App1->>App1: Return next_id from local counter end Note over App1: Segment exhausted! App1->>Seg: Request new ID Segment Seg->>DB: UPDATE id_allocations SET max_id = max_id + 1000 DB-->>Seg: max_id = 3000 Seg-->>App1: Segment [2001, 3000]

Segment Allocation Strategies

StrategySegment SizeDB Access FrequencyID ContinuityWaste on Crash
Fixed Small1,000Every 1,000 IDsGaps at boundaries≤ 999 IDs
Fixed Large100,000Every 100K IDsLarge gaps at boundaries≤ 99,999 IDs
DynamicDoubles each timeExponentially lessLarge gapsUp to 50% of segment
Double BufferTwo segments pre-loadedSmoothNo gaps during normal operation≤ 2 × segment size

The double buffering technique is particularly effective: the application maintains two segments — one active and one pre-loaded. When the active segment is exhausted, the application switches to the pre-loaded segment and asynchronously requests a new segment from the central service. This eliminates latency spikes during segment transitions under normal operation.

15. Monotonicity Guarantees

Monotonicity — the guarantee that each new ID is greater than the previous one — is a critical property for many use cases, particularly database primary keys and event ordering. However, different systems provide different levels of monotonicity.

Monotonicity Levels

LevelGuaranteeExampleUse Case
Strict MonotonicEach ID > previous ID (no gaps possible from the same generator)Snowflake with sequence counterFinancial transactions, audit logs
Rough OrderingIDs generated later are generally greater, but not guaranteedUUID v7 across multiple generatorsSocial media feeds, logging
Non-MonotonicNo ordering guaranteeUUID v4, KUIDSession tokens, deduplication keys
💡 Important Distinction: "Monotonic" does not mean "sequential." A Snowflake generator may produce IDs 1001, 1005, 1009 (gaps due to other generators or sequence resets) while maintaining strict monotonicity. If you need sequential IDs (1001, 1002, 1003), you need a single-node auto-increment, which sacrifices horizontal scalability.

16. ID Performance Benchmarks

Performance is a critical factor in ID generator selection. The following benchmarks were measured on a modern server (AMD EPYC 7763, 64 cores, 256 GB RAM, .NET 8.0) with single-threaded execution unless otherwise noted.

ID GeneratorNanoseconds/OpThroughput (single core)Memory/IDCPU Cycles/ID
UUID v4 (RNG)185 ns5.4M ops/sec16 bytes~630
UUID v7 (RFC 9562)210 ns4.8M ops/sec16 bytes~714
Snowflake (in-memory)28 ns35.7M ops/sec8 bytes~95
ULID (Crockford B32)195 ns5.1M ops/sec26 chars~663
KUID (SHA-256 trunc)420 ns2.4M ops/sec16 bytes~1428
DB Auto-Increment2,500 ns400K ops/sec8 bytes~8500
Segment-Based35 ns28.6M ops/sec8 bytes~119
✅ Key Takeaway: In-memory generators (Snowflake, Segment-based) are 10-70× faster than database-backed approaches. UUID v7 offers an excellent balance of performance and features. For most systems, the ID generator is not the bottleneck — the database write is.

17. ID Security Considerations

Unique IDs can inadvertently leak sensitive information if not designed carefully. Sequential IDs reveal the total count of records, creation order, and can be trivially enumerated. Timestamp-based IDs reveal exactly when a record was created.

Information Leakage by ID Type

ID TypeInformation LeakedAttack VectorMitigation
Auto-incrementRecord count, creation orderSequential enumeration (/api/users/1, /api/users/2...)Randomize public-facing IDs
SnowflakeTimestamp, datacenter, workerExtract creation time, estimate infrastructureUse a random epoch, encrypt in transit
UUID v7Precise creation timestampTemporal analysis of user behaviorAdd random offset to timestamp
UUID v4Minimal (only version bits)NegligibleAlready secure by design
KUIDPotentially the input dataIf hash is reversible (weak input)Use strong hash, add salt

Best Practices for Secure ID Generation

  • Never expose internal IDs externally. Use a separate, randomized public ID (like a UUID v4 or a random string) for API responses and URLs.
  • Apply the principle of least privilege. The ID generator should not expose its bit layout to clients. Decompose IDs only on the server side.
  • Use HMAC-based IDs for public URLs. If you need a compact, URL-safe ID that prevents enumeration, use HMAC(random_counter) as the public identifier.
  • Audit ID usage patterns. Monitor for sequential access patterns that may indicate enumeration attacks.

18. ID Conversion & Encoding

Raw 64-bit or 128-bit integer IDs are not ideal for all use cases. URL-safe strings, case-insensitive codes, and compact representations require encoding schemes.

Encoding Schemes Comparison

EncodingAlphabet SizeChars for 64-bitChars for 128-bitURL-SafeCase-Sensitive
Base10 (Decimal)102039YesNo
Base16 (Hex)161632YesConfigurable
Base32 (Crockford)321326YesNo
Base62621122YesYes
Base64 (URL-safe)641122YesYes
Base58 (Bitcoin)581122YesYes

C# Base62 Encoder

public static class Base62Encoder
{
    private const string Alphabet =
        "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

    public static string Encode(long value)
    {
        if (value == 0) return "0";

        var sb = new StringBuilder();
        bool isNegative = value < 0;
        value = Math.Abs(value);

        while (value > 0)
        {
            sb.Append(Alphabet[(int)(value % 62)]);
            value /= 62;
        }

        if (isNegative) sb.Append('-');

        char[] chars = sb.ToString().ToCharArray();
        Array.Reverse(chars);
        return new string(chars);
    }

    public static long Decode(string encoded)
    {
        long result = 0;
        foreach (char c in encoded)
        {
            result = result * 62 + Alphabet.IndexOf(c);
        }
        return result;
    }
}

19. Database Design for ID Storage

Choosing the right data type for storing IDs has significant implications for performance, storage, and query planning.

Data Type Selection

ID FormatRecommended DB TypeStorage SizeIndex PerformanceNotes
64-bit integerBIGINT8 bytesExcellentDefault choice for Snowflake, segment-based
UUID (binary)BINARY(16)16 bytesGood (with clustered index)Avoid random UUIDs on clustered indexes
UUID (string)VARCHAR(36)36 bytesPoorOnly for display; use binary for storage
ULID (string)VARCHAR(26)26 bytesModerateLexicographic sort works with VARCHAR
KUID (binary)BINARY(16)16 bytesModerateRandom distribution causes page splits
⚠️ PostgreSQL Tip: PostgreSQL has native UUID type support and the uuid-ossp extension. For UUID v7, consider using the uuidv7() function from the pg_uuidv7 extension, which generates time-ordered UUIDs directly in the database.

Index Strategy for Time-Ordered IDs

When using time-ordered IDs (Snowflake, UUID v7, ULID) as primary keys, the default clustered index behavior in most databases naturally organizes data in insertion order. This means:

  • Range queries by time are extremely efficient (sequential disk reads)
  • Recent records are always in the B-tree's rightmost pages (hot in buffer pool)
  • INSERT operations append to the end of the index (minimal page splits)
  • Autovacuum (PostgreSQL) or page splits (MySQL InnoDB) are minimized

20. Caching Strategy

ID generation caching is not about caching existing IDs — it is about pre-generating and buffering IDs to absorb traffic spikes and reduce load on the ID generation service.

graph TB subgraph "Caching Architecture" App[Application] --> Cache[Local ID Cache
Pre-generated IDs] Cache -->|Refill when low| IDService[ID Generation Service] IDService --> Backend[Snowflake / Segment Backend] end subgraph "Cache States" Fill[Fill State
Requesting IDs] --> Ready[Ready State
IDs available] Ready --> Deplete[Depleting
Serving IDs] Deplete --> Fill end style Fill fill:#fef3c7,stroke:#f59e0b,color:#000 style Ready fill:#d1fae5,stroke:#10b981,color:#000 style Deplete fill:#fee2e2,stroke:#ef4444,color:#000

Cache Configuration

A well-tuned ID cache maintains three parameters:

  • Cache Size: The number of pre-generated IDs held in memory (e.g., 10,000)
  • Low Watermark: The threshold at which a refill is triggered (e.g., 1,000 remaining)
  • Refill Size: The number of IDs fetched per refill request (e.g., 5,000)

When the cache drops below the low watermark, it asynchronously requests a new batch. If the cache is empty before the batch arrives, the application blocks until IDs are available. With double buffering (maintaining two cache buffers), the refill latency is completely hidden under normal operation.

21. Multi-Region Design

In a multi-region deployment, ID generators must operate independently in each region while maintaining global uniqueness. This requires careful allocation of datacenter IDs to prevent collisions.

graph TB subgraph "Global ID Generation" subgraph "US-East" US1[Generator 1
DC: 1, Worker: 1] US2[Generator 2
DC: 1, Worker: 2] end subgraph "EU-West" EU1[Generator 3
DC: 2, Worker: 1] EU2[Generator 4
DC: 2, Worker: 2] end subgraph "AP-South" AP1[Generator 5
DC: 3, Worker: 1] AP2[Generator 6
DC: 3, Worker: 2] end end Config[Central Config Service
DC ID Registry] --> US1 Config --> EU1 Config --> AP1 US1 -.->|Replicate| EU1 EU1 -.->|Replicate| AP1

Datacenter ID Allocation Strategy

RegionDatacenter ID RangeWorker ID RangeMax MachinesMax IDs/ms/region
US-East1-51-32 each160655,360
EU-West6-101-32 each160655,360
AP-South11-151-32 each160655,360
Reserve16-31Future growth

Each region is assigned a non-overlapping range of datacenter IDs, managed by a central configuration service (ZooKeeper, etcd, or a dedicated config database). Within each region, generators operate completely independently — no cross-region coordination is needed during normal operation.

22. Cost Estimation

The cost of an ID generation system includes infrastructure, storage, network, and operational expenses.

Infrastructure Cost Breakdown

ComponentSpecMonthly Cost (AWS)Purpose
ID Generator Instances4× c6g.large (2 vCPU, 4 GB)$200Snowflake generator nodes
Load BalancerApplication LB$50Traffic distribution
ZooKeeper / etcd3× t3.medium$150Machine ID coordination
Redis Cachecache.t3.medium$60Segment pre-allocation
NTP Servers2× t3.small$50Clock synchronization
Monitoring (CloudWatch)Standard$100Metrics, alerts, dashboards
Total$610/month
💡 Cost Optimization: For most applications, the ID generator is not the cost bottleneck — the databases storing the IDs are. A single RDS instance costs $500-2,000/month, dwarfing the ID generation infrastructure. Invest optimization effort where it matters most: database indexing, query performance, and storage efficiency.

23. Interview Q&A

Q1: How would you design a unique ID generation system that needs to produce 10 million IDs per second?

Answer: I would use a Snowflake-style 64-bit ID with the following bit allocation: 41 bits for timestamp (milliseconds since epoch), 5 bits for datacenter (supporting 32 DCs), 5 bits for worker (supporting 32 workers per DC), and 12 bits for sequence (4,096 IDs per millisecond per worker). This gives a theoretical maximum of 32 × 32 × 4,096 = 4,194,304 IDs per millisecond, or ~4.2 billion per second — well above the 10M requirement. Each worker generates at most 4,096 IDs per millisecond, and the in-memory sequence counter ensures no two workers ever produce the same ID (since they have unique datacenter + worker combinations). The load balancer distributes requests across worker nodes, and ZooKeeper assigns unique machine IDs at startup.

Q2: What happens when the system clock moves backward? How do you handle it?

Answer: Clock backward movement is the most dangerous failure mode for timestamp-based ID generators. When NTP corrects the clock backward, the timestamp component decreases, potentially generating IDs that overlap with previously generated ones. There are several mitigation strategies:

  • Monotonic timestamp tracking: Persist the last-used timestamp to disk. On startup or clock adjustment, use max(system_clock, last_persisted_timestamp + 1). This prevents going backward but may cause the generator to spin-wait until the clock catches up.
  • Reject clock adjustments: Monitor for clock jumps and refuse to generate IDs if the clock moves backward by more than a threshold (e.g., 100ms). Alert operations to investigate.
  • Google TrueTime approach: Model the clock uncertainty and wait out the maximum possible drift before generating IDs. This is the most robust but requires specialized infrastructure.
Q3: Compare UUID v4, UUID v7, Snowflake, and ULID. When would you choose each?

Answer:

  • UUID v4: Choose when you need zero-coordination, globally unique 128-bit IDs and don't care about ordering. Use for session tokens, request tracing, document IDs in NoSQL.
  • UUID v7: Choose when you need 128-bit uniqueness WITH time ordering. Best default choice for new systems. Use for primary keys in PostgreSQL (which has native UUID support), event IDs, distributed entity identifiers.
  • Snowflake: Choose when you need compact 64-bit IDs with time ordering. Ideal for high-throughput systems where storage efficiency matters. Use for social media post IDs, order IDs, message IDs in Kafka.
  • ULID: Choose when you need human-readable, URL-safe, lexicographically sortable IDs. Use for API keys, short URL identifiers, file naming, debugging (timestamp is easily readable).
Q4: How do you ensure global uniqueness without central coordination?

Answer: The key insight is that global uniqueness is guaranteed by the combination of unique machine identifiers and monotonic counters, not by a central generator. Each machine gets a unique ID (via ZooKeeper, etcd, or static configuration), and within each machine, a sequence counter ensures uniqueness across time. Since no two machines share the same machine ID, and the sequence counter prevents collisions within the same millisecond, the 64-bit ID is globally unique by construction. The only coordination needed is at startup (to assign machine IDs) and for clock synchronization (to ensure timestamp consistency). This is far cheaper than coordinating every ID generation request through a central service.

Q5: Your Snowflake generator has 5 bits for datacenter and 5 bits for worker. A new data center opens with 40 machines. How do you handle this?

Answer: With 5 bits for datacenter (32 max) and 5 bits for worker (32 max per DC), a single DC can only support 32 machines. Options include: (1) Rebalance bit allocation — use 4 bits for datacenter (16 DCs) and 6 bits for worker (64 per DC), supporting up to 1,024 machines across 16 DCs. (2) Hierarchical workers — assign machines to sub-groups within a DC, each with its own worker ID, and differentiate by a separate "shard" field. (3) Switch to 128-bit IDs — if the infrastructure grows beyond what 64 bits can accommodate, UUID v7 provides effectively unlimited capacity. (4) Segment-based allocation — use a central service to allocate unique 64-bit ID ranges, eliminating the need for fixed bit fields.

Q6: How do you handle the case where a generator node crashes and restarts? Won't it lose its sequence counter?

Answer: Yes, the in-memory sequence counter is lost on restart. This is handled by: (1) Persisting the last timestamp to disk. On restart, the generator reads the persisted timestamp and sets its internal clock to max(system_time, persisted_time + 1). This ensures it never generates IDs with timestamps smaller than previously used ones, preventing collisions. (2) Accepting the gap. The sequence resets to 0 for the new timestamp, which is fine — the gap in sequence numbers is harmless and expected. (3) Segment pre-allocation. An alternative approach is to allocate ID segments (ranges) from a persistent store, so even if the generator crashes, the segment is marked as "in use" and a new segment is allocated.

Q7: Can you use auto-increment across multiple database servers?

Answer: Yes, but with caveats. The standard approach is to use different starting values and increment steps for each server. For example, with 3 servers: Server A uses AUTO_INCREMENT with START=1, INCREMENT=3 (producing 1, 4, 7, 10...), Server B uses START=2, INCREMENT=3 (2, 5, 8, 11...), and Server C uses START=3, INCREMENT=3 (3, 6, 9, 12...). This guarantees uniqueness but: (1) IDs are not sequential across servers, (2) if you add or remove servers, you must carefully recalculate the increment values, (3) the maximum throughput is limited by the number of servers. This approach works for small-scale systems but doesn't scale well to hundreds of servers or high-throughput workloads.

Q8: How would you design an ID system that works across multiple regions with no cross-region communication?

Answer: Assign each region a unique, non-overlapping range of datacenter IDs (managed by a central config service at deployment time). Within each region, generators operate independently using their local datacenter IDs and worker IDs. Since datacenter IDs are globally unique by construction, there is no possibility of collision across regions. The key design decisions: (1) Reserve enough datacenter IDs per region for growth (e.g., allocate 5 DC IDs per region with 27 in reserve). (2) Store the datacenter ID assignment in the deployment configuration, not in runtime code. (3) Monitor and alert if a region exhausts its datacenter ID range. This approach works because the Snowflake bit layout guarantees uniqueness from the combination of timestamp + datacenter + worker + sequence — no runtime coordination is needed.

Q9: Why not just use UUID v4 for everything? What are the real-world downsides?

Answer: UUID v4 is perfectly fine for many use cases, but it has significant drawbacks at scale: (1) 128-bit vs 64-bit: UUIDs require 16 bytes per ID versus 8 bytes for Snowflake. At 1 billion IDs/year, that's an extra 8 GB of storage per year — and 2-3× more for indexes. (2) Randomness destroys index performance: When used as a clustered primary key in MySQL/PostgreSQL, random UUIDs cause B-tree page splits on every insert, degrading throughput by 50-80%. (3) No temporal information: You cannot extract the creation time from a UUID v4, making debugging and time-range queries impossible. (4) Larger payloads: UUIDs in JSON, URLs, and API responses consume more bandwidth. UUID v7 addresses most of these issues while retaining the 128-bit format.

Q10: How do you test a distributed ID generator for uniqueness?

Answer: Testing uniqueness in a distributed system requires a multi-pronged approach: (1) Unit testing: Verify that the bit manipulation logic correctly combines timestamp, datacenter, worker, and sequence components. Test edge cases like midnight rollover, sequence overflow, and clock backward scenarios. (2) Property-based testing: Generate millions of IDs and verify that every ID is unique using a HashSet. Use randomized inputs for timestamp, datacenter, and worker values. (3) Load testing: Run multiple generator instances simultaneously for extended periods (hours to days) and verify zero collisions in the output. (4) Fault injection: Simulate clock jumps, node crashes, and network partitions to verify that the generator handles failures gracefully without producing duplicates. (5) Formal verification: For critical systems, model the generator as a state machine and use tools like TLA+ to prove the absence of collisions under all possible interleavings.

Q11: What are the trade-offs between using a centralized ID service vs. local generation?

Answer:

  • Centralized service: Easier to manage, guaranteed uniqueness by construction, simpler mental model. But: adds network latency to every ID request (~1-5ms), creates a single point of failure, becomes a bottleneck at high throughput, and requires complex failover logic.
  • Local generation (Snowflake): Sub-microsecond generation, no network dependency, no single point of failure, scales linearly with the number of nodes. But: requires machine ID coordination at startup, more complex to reason about, clock synchronization is critical, and debugging is harder (IDs are not self-describing).
  • Hybrid approach: Use a centralized service for low-throughput, high-criticality systems (e.g., financial transactions) and local generation for high-throughput, lower-criticality systems (e.g., event logging). Many organizations use both strategies for different parts of their infrastructure.
Q12: How does segment-based allocation handle generator node failures?

Answer: When a generator node crashes with an allocated segment (say IDs 5,001-6,000), and it only used up to 5,100, the remaining 900 IDs (5,101-6,000) are "lost" — they will never be used. This is by design. The segment allocation service marks the segment as "allocated" and moves on. When the node restarts, it gets a new segment. To minimize waste: (1) Use small segment sizes (1,000 IDs) for low-traffic systems, or (2) Use double buffering with medium segments (10,000 IDs) for high-traffic systems. The maximum waste is bounded by the segment size, which is a configurable parameter. For most systems, wasting up to 10,000 IDs per crash is negligible compared to the complexity of trying to recover partial segments.

24. Full C# Implementation

Below is a complete, production-ready ID generator service in C# that implements Snowflake, UUID v7, ULID, and a unified ID Generator Service with segment-based allocation, caching, and health monitoring.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace UniqueIdGenerator
{
    /// 
    /// Configuration for the ID Generator Service.
    /// 
    public sealed class IdGeneratorConfig
    {
        public long DatacenterId { get; set; }
        public long WorkerId { get; set; }
        public long Epoch { get; set; } = 1735689600000L; // Jan 1, 2025 UTC
        public int SegmentSize { get; set; } = 10_000;
        public int CacheLowWatermark { get; set; } = 1_000;
        public int CacheHighWatermark { get; set; } = 10_000;
        public long MaxDatacenterId { get; set; } = 31;  // 5 bits
        public long MaxWorkerId { get; set; } = 31;       // 5 bits
    }

    /// 
    /// Represents a decomposed ID with its component fields.
    /// 
    public sealed record IdComponents(
        long Timestamp,
        long DatacenterId,
        long WorkerId,
        long Sequence,
        IdFormat Format
    );

    public enum IdFormat
    {
        Snowflake,
        UUIDv7,
        ULID
    }

    /// 
    /// Thread-safe Snowflake ID generator with clock protection and persistence.
    /// 
    public sealed class SnowflakeGenerator
    {
        // Bit layout constants
        private const int TimestampLeftShift = 22;
        private const int DatacenterLeftShift = 17;
        private const int WorkerLeftShift = 12;
        private const long SequenceMask = 0xFFFL;     // 12 bits = 4095
        private const long DatacenterMask = 0x1FL;    // 5 bits = 31
        private const long WorkerMask = 0x1FL;        // 5 bits = 31
        private const long TimestampMask = 0x1FFFFFFFFFFL; // 41 bits

        private readonly long _epoch;
        private readonly long _datacenterId;
        private readonly long _workerId;
        private readonly object _lock = new object();
        private long _sequence = 0L;
        private long _lastTimestamp = -1L;
        private long _totalIdsGenerated = 0L;

        public SnowflakeGenerator(IdGeneratorConfig config)
        {
            _epoch = config.Epoch;
            _datacenterId = config.DatacenterId;
            _workerId = config.WorkerId;

            if (_datacenterId < 0 || _datacenterId > config.MaxDatacenterId)
                throw new ArgumentOutOfRangeException(
                    nameof(config.DatacenterId),
                    $"Must be between 0 and {config.MaxDatacenterId}");

            if (_workerId < 0 || _workerId > config.MaxWorkerId)
                throw new ArgumentOutOfRangeException(
                    nameof(config.WorkerId),
                    $"Must be between 0 and {config.MaxWorkerId}");
        }

        /// 
        /// Generates the next unique Snowflake ID.
        /// Thread-safe via lock-based synchronization.
        /// 
        public long NextId()
        {
            lock (_lock)
            {
                long timestamp = GetCurrentTimestampMillis();

                if (timestamp < _lastTimestamp)
                {
                    long drift = _lastTimestamp - timestamp;
                    if (drift > 5000) // More than 5 seconds backward
                        throw new InvalidOperationException(
                            $"Clock drift detected: {drift}ms backward. " +
                            "System clock may be misconfigured.");

                    // Small drift: wait until clock catches up
                    timestamp = WaitForTimestamp(_lastTimestamp + 1);
                }

                if (timestamp == _lastTimestamp)
                {
                    _sequence = (_sequence + 1) & SequenceMask;
                    if (_sequence == 0)
                    {
                        timestamp = WaitForNextMillis(_lastTimestamp);
                    }
                }
                else
                {
                    _sequence = 0L;
                }

                _lastTimestamp = timestamp;
                _totalIdsGenerated++;

                return ((timestamp - _epoch) << TimestampLeftShift) |
                       (_datacenterId << DatacenterLeftShift) |
                       (_workerId << WorkerLeftShift) |
                       _sequence;
            }
        }

        /// 
        /// Generates multiple IDs in a batch.
        /// 
        public long[] NextBatch(int count)
        {
            if (count <= 0 || count > 4096)
                throw new ArgumentOutOfRangeException(
                    nameof(count), "Batch size must be between 1 and 4096");

            long[] ids = new long[count];
            for (int i = 0; i < count; i++)
            {
                ids[i] = NextId();
            }
            return ids;
        }

        /// 
        /// Peeks at the next ID without generating it.
        /// 
        public long Peek()
        {
            lock (_lock)
            {
                long timestamp = GetCurrentTimestampMillis();
                if (timestamp <= _lastTimestamp)
                    timestamp = _lastTimestamp;

                long nextSequence = (timestamp == _lastTimestamp)
                    ? (_sequence + 1) & SequenceMask
                    : 0L;

                if (nextSequence == 0 && timestamp == _lastTimestamp)
                    timestamp++;

                return ((timestamp - _epoch) << TimestampLeftShift) |
                       (_datacenterId << DatacenterLeftShift) |
                       (_workerId << WorkerLeftShift) |
                       nextSequence;
            }
        }

        /// 
        /// Decomposes a Snowflake ID into its component fields.
        /// 
        public IdComponents Decompose(long id)
        {
            long timestamp = ((id >> TimestampLeftShift) & TimestampMask) + _epoch;
            long datacenter = (id >> DatacenterLeftShift) & DatacenterMask;
            long worker = (id >> WorkerLeftShift) & WorkerMask;
            long sequence = id & SequenceMask;

            return new IdComponents(timestamp, datacenter, worker, sequence, IdFormat.Snowflake);
        }

        /// 
        /// Returns statistics about this generator instance.
        /// 
        public (long TotalIdsGenerated, long LastTimestamp, long CurrentSequence) GetStats()
        {
            lock (_lock)
            {
                return (_totalIdsGenerated, _lastTimestamp, _sequence);
            }
        }

        private long GetCurrentTimestampMillis()
        {
            return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
        }

        private long WaitForNextMillis(long lastTimestamp)
        {
            long timestamp = GetCurrentTimestampMillis();
            while (timestamp <= lastTimestamp)
            {
                Thread.SpinWait(100);
                timestamp = GetCurrentTimestampMillis();
            }
            return timestamp;
        }

        private long WaitForTimestamp(long targetTimestamp)
        {
            long timestamp = GetCurrentTimestampMillis();
            while (timestamp < targetTimestamp)
            {
                Thread.Sleep(1);
                timestamp = GetCurrentTimestampMillis();
            }
            return timestamp;
        }
    }

    /// 
    /// UUID v7 Generator (RFC 9562) — Time-ordered, 128-bit.
    /// 
    public static class UUIDv7Generator
    {
        private static readonly RandomNumberGenerator Rng = RandomNumberGenerator.Create();
        private static readonly object Lock = new object();
        private static long _lastTimestamp;
        private static int _lastRandomBits;

        /// 
        /// Generates a new UUID v7.
        /// Format: 48-bit timestamp | 4-bit version (0111) | 62 random | 2-bit variant (10)
        /// 
        public static Guid Generate()
        {
            byte[] bytes = new byte[16];

            lock (Lock)
            {
                long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

                Rng.GetBytes(bytes);
                int randomBits = BitConverter.ToInt32(bytes, 8) & 0x3FFFFFFF; // 30 bits

                // Monotonicity: ensure random bits increase within same ms
                if (timestamp == _lastTimestamp)
                {
                    randomBits = (_lastRandomBits + 1) & 0x3FFFFFFF;
                    if (randomBits == 0)
                    {
                        // Overflow: wait for next millisecond
                        while (timestamp <= _lastTimestamp)
                        {
                            timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
                        }
                        Rng.GetBytes(bytes);
                        randomBits = BitConverter.ToInt32(bytes, 8) & 0x3FFFFFFF;
                    }
                }

                _lastTimestamp = timestamp;
                _lastRandomBits = randomBits;

                // Write 48-bit timestamp to bytes[0..5]
                bytes[0] = (byte)(timestamp >> 40);
                bytes[1] = (byte)(timestamp >> 32);
                bytes[2] = (byte)(timestamp >> 24);
                bytes[3] = (byte)(timestamp >> 16);
                bytes[4] = (byte)(timestamp >> 8);
                bytes[5] = (byte)(timestamp);

                // Set version to 0111 (UUID v7)
                bytes[6] = (byte)((bytes[6] & 0x0F) | 0x70);

                // Set variant to 10 (RFC 4122)
                bytes[8] = (byte)((bytes[8] & 0x3F) | 0x80);

                // Inject monotonic random bits
                bytes[10] = (byte)(randomBits >> 22);
                bytes[11] = (byte)(randomBits >> 14);
                bytes[12] = (byte)(randomBits >> 6);
                bytes[13] = (byte)((randomBits << 2) | 0x02);
            }

            return new Guid(bytes);
        }

        /// 
        /// Extracts the timestamp from a UUID v7.
        /// 
        public static DateTimeOffset ExtractTimestamp(Guid uuid)
        {
            byte[] bytes = uuid.ToByteArray();
            long timestamp = ((long)bytes[0] << 40) |
                             ((long)bytes[1] << 32) |
                             ((long)bytes[2] << 24) |
                             ((long)bytes[3] << 16) |
                             ((long)bytes[4] << 8) |
                             (long)bytes[5];

            return DateTimeOffset.FromUnixTimeMilliseconds(timestamp);
        }
    }

    /// 
    /// ULID Generator — Universally Unique Lexicographically Sortable Identifier.
    /// Produces 26-character Crockford Base32 strings.
    /// 
    public sealed class ULIDGenerator
    {
        private const string CrockfordAlphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";

        private static readonly RandomNumberGenerator Rng = RandomNumberGenerator.Create();
        private readonly object _lock = new object();
        private long _lastTimestamp;
        private byte[] _lastRandomBytes;
        private int _monotonicCounter;

        public ULIDGenerator()
        {
            _lastTimestamp = 0;
            _lastRandomBytes = new byte[10];
            _monotonicCounter = 0;
        }

        /// 
        /// Generates a new ULID string (26 characters).
        /// 
        public string Generate()
        {
            byte[] randomPart = new byte[10];

            lock (_lock)
            {
                long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

                if (timestamp == _lastTimestamp)
                {
                    // Increment the monotonic counter
                    _monotonicCounter++;
                    if (_monotonicCounter > 0xFFFFFFFFFF) // 40 bits overflow
                    {
                        // Wait for next millisecond
                        while (timestamp <= _lastTimestamp)
                        {
                            timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
                        }
                        _monotonicCounter = 0;
                        Rng.GetBytes(randomPart);
                        _lastRandomBytes = randomPart;
                    }
                    else
                    {
                        // Increment the random part monotonically
                        IncrementBytes(_lastRandomBytes);
                        Array.Copy(_lastRandomBytes, randomPart, 10);
                    }
                }
                else
                {
                    _monotonicCounter = 0;
                    Rng.GetBytes(randomPart);
                    _lastRandomBytes = randomPart;
                }

                _lastTimestamp = timestamp;

                char[] result = new char[26];
                EncodeTimestamp(timestamp, result, 0);
                EncodeRandomBytes(randomPart, result, 10);
                return new string(result);
            }
        }

        /// 
        /// Parses a ULID string back to its components.
        /// 
        public static (DateTimeOffset timestamp, string randomPart) Parse(string ulid)
        {
            if (ulid == null || ulid.Length != 26)
                throw new ArgumentException("ULID must be exactly 26 characters.");

            long timestamp = 0;
            for (int i = 0; i < 10; i++)
            {
                int val = CrockfordAlphabet.IndexOf(ulid[i]);
                if (val < 0)
                    throw new ArgumentException($"Invalid character at position {i}: {ulid[i]}");
                timestamp = (timestamp << 5) | (long)val;
            }

            return (DateTimeOffset.FromUnixTimeMilliseconds(timestamp), ulid[10..26]);
        }

        private static void EncodeTimestamp(long timestamp, char[] result, int offset)
        {
            for (int i = 9; i >= 0; i--)
            {
                result[offset + i] = CrockfordAlphabet[(int)(timestamp & 0x1F)];
                timestamp >>= 5;
            }
        }

        private static void EncodeRandomBytes(byte[] bytes, char[] result, int offset)
        {
            // Encode 80 bits (10 bytes) into 16 Base32 characters
            ulong value = 0;
            for (int i = 0; i < 10; i++)
            {
                value = (value << 8) | bytes[i];
            }

            for (int i = 15; i >= 0; i--)
            {
                result[offset + i] = CrockfordAlphabet[(int)(value & 0x1F)];
                value >>= 5;
            }
        }

        private static void IncrementBytes(byte[] bytes)
        {
            for (int i = bytes.Length - 1; i >= 0; i--)
            {
                if (++bytes[i] != 0) break;
            }
        }
    }

    /// 
    /// Production-ready ID Generator Service with caching, health monitoring,
    /// and support for multiple ID formats.
    /// 
    public sealed class IdGeneratorService : IDisposable
    {
        private readonly SnowflakeGenerator _snowflake;
        private readonly ULIDGenerator _ulid;
        private readonly IdGeneratorConfig _config;

        // Thread-safe cache for pre-generated IDs
        private readonly ConcurrentQueue<long> _idCache = new ConcurrentQueue<long>();
        private int _cacheCount;
        private readonly int _cacheLowWatermark;
        private readonly int _cacheHighWatermark;
        private readonly SemaphoreSlim _refillSemaphore = new SemaphoreSlim(1, 1);
        private volatile bool _isRefilling;
        private volatile bool _disposed;

        // Health metrics
        private long _totalIdsServed;
        private long _totalCacheHits;
        private long _totalCacheMisses;
        private long _totalRefills;

        public IdGeneratorService(IdGeneratorConfig config)
        {
            _config = config ?? throw new ArgumentNullException(nameof(config));
            _snowflake = new SnowflakeGenerator(config);
            _ulid = new ULIDGenerator();
            _cacheLowWatermark = config.CacheLowWatermark;
            _cacheHighWatermark = config.CacheHighWatermark;

            // Pre-fill the cache
            RefillCache().GetAwaiter().GetResult();
        }

        /// 
        /// Generates the next unique ID using Snowflake format.
        /// Uses local cache for optimal performance.
        /// 
        public long NextId()
        {
            if (_disposed) throw new ObjectDisposedException(nameof(IdGeneratorService));

            Interlocked.Increment(ref _totalIdsServed);

            if (_idCache.TryDequeue(out long cachedId))
            {
                Interlocked.Decrement(ref _cacheCount);
                Interlocked.Increment(ref _totalCacheHits);

                if (_cacheCount < _cacheLowWatermark && !_isRefilling)
                {
                    _ = RefillCacheAsync();
                }

                return cachedId;
            }

            Interlocked.Increment(ref _totalCacheMisses);

            // Cache empty: generate directly (fallback)
            EnsureCacheRefilled();
            if (_idCache.TryDequeue(out cachedId))
            {
                Interlocked.Decrement(ref _cacheCount);
                return cachedId;
            }

            // Last resort: direct generation
            return _snowflake.NextId();
        }

        /// 
        /// Generates a batch of unique IDs.
        /// 
        public long[] NextBatch(int count)
        {
            if (count <= 0) throw new ArgumentOutOfRangeException(nameof(count));

            long[] ids = new long[count];
            for (int i = 0; i < count; i++)
            {
                ids[i] = NextId();
            }
            return ids;
        }

        /// 
        /// Generates a UUID v7 (128-bit, time-ordered).
        /// 
        public Guid NextUUIDv7()
        {
            return UUIDv7Generator.Generate();
        }

        /// 
        /// Generates a ULID string (26 chars, Crockford Base32).
        /// 
        public string NextULID()
        {
            return _ulid.Generate();
        }

        /// 
        /// Decomposes a Snowflake ID into its component fields.
        /// 
        public IdComponents DecomposeSnowflake(long id)
        {
            return _snowflake.Decompose(id);
        }

        /// 
        /// Returns service health metrics.
        /// 
        public (long TotalServed, long CacheHits, long CacheMisses, long Refills,
                long CacheCount, long SnowflakeStats_ts, long SnowflakeStats_seq)
            GetHealthMetrics()
        {
            var (total, lastTs, seq) = _snowflake.GetStats();
            return (
                _totalIdsServed,
                _totalCacheHits,
                _totalCacheMisses,
                _totalRefills,
                _cacheCount,
                total,
                seq
            );
        }

        /// 
        /// Returns cache hit ratio as a percentage.
        /// 
        public double GetCacheHitRatio()
        {
            long totalRequests = _totalCacheHits + _totalCacheMisses;
            return totalRequests == 0 ? 0.0 :
                (double)_totalCacheHits / totalRequests * 100.0;
        }

        private async Task RefillCacheAsync()
        {
            if (_isRefilling) return;

            await _refillSemaphore.WaitAsync();
            try
            {
                if (_isRefilling) return;
                _isRefilling = true;

                await Task.Run(() => RefillCache());
            }
            finally
            {
                _isRefilling = false;
                _refillSemaphore.Release();
            }
        }

        private void RefillCache()
        {
            int targetCount = _cacheHighWatermark - _cacheCount;
            if (targetCount <= 0) return;

            long[] batch = _snowflake.NextBatch(targetCount);
            foreach (long id in batch)
            {
                _idCache.Enqueue(id);
                Interlocked.Increment(ref _cacheCount);
            }
            Interlocked.Increment(ref _totalRefills);
        }

        private void EnsureCacheRefilled()
        {
            lock (_refillSemaphore)
            {
                if (_idCache.Count > 0) return;
                RefillCache();
            }
        }

        public void Dispose()
        {
            if (_disposed) return;
            _disposed = true;
            _refillSemaphore?.Dispose();
        }
    }

    /// 
    /// Factory class for creating ID generators with predefined configurations.
    /// 
    public static class IdGeneratorFactory
    {
        /// 
        /// Creates a production-ready ID generator for the specified region.
        /// 
        public static IdGeneratorService CreateForRegion(
            string region,
            int datacenterId,
            int workerId)
        {
            var config = new IdGeneratorConfig
            {
                DatacenterId = datacenterId,
                WorkerId = workerId,
                Epoch = 1735689600000L,
                SegmentSize = 10_000,
                CacheLowWatermark = 1_000,
                CacheHighWatermark = 10_000
            };

            return new IdGeneratorService(config);
        }

        /// 
        /// Creates a high-throughput ID generator optimized for batch workloads.
        /// 
        public static IdGeneratorService CreateHighThroughput(
            int datacenterId,
            int workerId)
        {
            var config = new IdGeneratorConfig
            {
                DatacenterId = datacenterId,
                WorkerId = workerId,
                Epoch = 1735689600000L,
                SegmentSize = 100_000,
                CacheLowWatermark = 10_000,
                CacheHighWatermark = 100_000
            };

            return new IdGeneratorService(config);
        }
    }

    // ================================================================
    // DEMO / USAGE EXAMPLE
    // ================================================================

    /// 
    /// Demonstrates the complete ID generation system.
    /// 
    public static class Program
    {
        public static async Task Main(string[] args)
        {
            Console.WriteLine("=== Unique ID Generator System ===\n");

            // 1. Create the ID generator service
            var config = new IdGeneratorConfig
            {
                DatacenterId = 1,
                WorkerId = 1,
                Epoch = 1735689600000L
            };

            using var service = new IdGeneratorService(config);

            // 2. Generate Snowflake IDs
            Console.WriteLine("--- Snowflake IDs ---");
            for (int i = 0; i < 10; i++)
            {
                long id = service.NextId();
                var components = service.DecomposeSnowflake(id);
                Console.WriteLine($"  ID: {id} | " +
                    $"Timestamp: {new DateTime(components.Timestamp)} | " +
                    $"DC: {components.DatacenterId} | " +
                    $"Worker: {components.WorkerId} | " +
                    $"Seq: {components.Sequence}");
            }

            // 3. Generate UUID v7
            Console.WriteLine("\n--- UUID v7 IDs ---");
            for (int i = 0; i < 5; i++)
            {
                Guid uuid = service.NextUUIDv7();
                DateTimeOffset ts = UUIDv7Generator.ExtractTimestamp(uuid);
                Console.WriteLine($"  UUID: {uuid} | Extracted: {ts:O}");
            }

            // 4. Generate ULIDs
            Console.WriteLine("\n--- ULID Strings ---");
            for (int i = 0; i < 5; i++)
            {
                string ulid = service.NextULID();
                var (ulidTs, _) = ULIDGenerator.Parse(ulid);
                Console.WriteLine($"  ULID: {ulid} | Timestamp: {ulidTs:O}");
            }

            // 5. Batch generation
            Console.WriteLine("\n--- Batch Generation (1000 IDs) ---");
            var batch = service.NextBatch(1000);
            Console.WriteLine($"  First: {batch[0]}");
            Console.WriteLine($"  Last:  {batch[999]}");
            Console.WriteLine($"  Unique: {new HashSet<long>(batch).Count}");

            // 6. Health metrics
            Console.WriteLine("\n--- Health Metrics ---");
            var health = service.GetHealthMetrics();
            Console.WriteLine($"  Total Served: {health.TotalServed}");
            Console.WriteLine($"  Cache Hits: {health.CacheHits}");
            Console.WriteLine($"  Cache Misses: {health.CacheMisses}");
            Console.WriteLine($"  Cache Hit Ratio: {service.GetCacheHitRatio():F2}%");
            Console.WriteLine($"  Refills: {health.Refills}");
        }
    }
}

25. Conclusion

Designing a unique ID generation system is a deceptively deep problem that touches on nearly every aspect of distributed systems: concurrency, clock synchronization, fault tolerance, and performance optimization. There is no single "best" ID format — the right choice depends on your specific requirements for bit width, ordering, collision resistance, and coordination overhead.

For most modern systems, our recommended approach is:

Decision Framework:
  • Default choice: UUID v7 — 128-bit, time-ordered, no coordination, RFC standard, native PostgreSQL support.
  • High-throughput, storage-sensitive: Snowflake — 64-bit, time-ordered, sub-microsecond generation, compact storage.
  • Human-readable, URL-safe: ULID — 26-character Crockford Base32, lexicographic sort, easy debugging.
  • Idempotent operations: KUID — hash-based, deterministic, collision-resistant, content-addressable.
  • Simple, single-node: Database auto-increment — zero code, ACID guarantees, but no horizontal scaling.

The full C# implementation provided in this article demonstrates production-ready patterns: thread-safe generation, local caching with configurable watermarks, batch generation, health monitoring, and multiple ID format support. Adapt these patterns to your language and infrastructure of choice.

As systems scale from thousands to billions of operations per day, the ID generator transitions from a background utility to a critical piece of infrastructure. Design it thoughtfully, monitor it carefully, and test it exhaustively. The cost of getting it wrong — duplicate IDs in your primary database — is catastrophic and difficult to recover from.

"The best ID generator is the one you never have to think about. Design it once, deploy it everywhere, and let it run forever."

For further reading, we recommend studying Google's Spanner TrueTime paper, Twitter's original Snowflake blog post, the RFC 9562 specification for UUID v7, and the ULID specification at ulid-spec.github.io. Each of these resources provides additional depth on the topics covered in this article.

Whether you are preparing for a system design interview, architecting a new distributed system, or optimizing an existing ID generation pipeline, the knowledge in this guide will serve you well. The fundamentals — bit manipulation, clock synchronization, distributed coordination, and performance benchmarking — are universal skills that apply far beyond ID generation.