# System Design: [Unique ID generator name]

> Reference design for a distributed unique ID generator: when to use UUID
> version 4 or version 7 (RFC 9562, May 2024), how a Snowflake-style 64-bit
> layout works, how to survive clock skew and clocks that move backwards, how
> to assign worker IDs safely, what sortability buys for database indexes,
> why JavaScript clients need IDs as strings above $2^{53}$, and when a
> database ticket server is still the simplest answer. It follows the
> structure of the System Design Document template, adds a 45-minute
> interview path, access-pattern and failure-mode tables, and is pre-filled
> with a worked design to adapt. Replace every `[bracketed]` value and every
> row marked *Example*; values marked *Example* are assumptions, not
> benchmarks.

<!--
Guidance: How to use this template
- Decide first what the ID must do besides being unique: sort by time, fit
  in 64 bits, hide business volume, be typed by humans, or be generated
  offline on a device. Each property rules out some schemes.
- Fill the peak generation rate and the number of generating processes in
  section 3 before picking a layout; the worker bits run out before the
  sequence bits do.
- Treat the clock as an input that can be wrong. Write down what the
  generator does when time goes backwards before writing code.
- Decide the wire format (number or string) with the client teams on day one;
  changing it later breaks every API consumer.
-->

## Document control

<!-- Guidance: A design is a proposal until it is approved; record who decided and when. -->

| Field | Value |
|---|---|
| Document ID | [SDD-UID-001] |
| System / feature | [Distributed ID generation] |
| Version | [0.1] |
| Status | [Draft / In review / Approved / Implemented / Superseded] |
| Owner | [@owner] |
| Author(s) | [@author] |
| Reviewers | [@reviewer-1], [@reviewer-2] |
| Last updated | YYYY-MM-DD |
| Classification | [Internal] |
| ID formats in use | [UUIDv7 / Snowflake-style 64-bit / ticket server] |
| Custom epoch | [YYYY-MM-DDTHH:MM:SSZ] |
| Related | [Database schema standards], [API style guide], [ADR: ID format] |

### Approvals

| Role | Name | Decision | Date |
|---|---|---|---|
| Tech lead | [@name] | [Approved / Changes requested] | YYYY-MM-DD |
| Staff / principal engineer | [@name] | [Decision] | YYYY-MM-DD |
| Database owner | [@name] | [Decision] | YYYY-MM-DD |
| API and client leads | [@name] | [Decision] | YYYY-MM-DD |
| SRE / operations | [@name] | [Decision] | YYYY-MM-DD |

## Revision history

<!-- Guidance: Note what changed in the design, not the wording. -->

| Version | Date | Author | Change summary |
|---|---|---|---|
| 0.1 | YYYY-MM-DD | [@name] | First draft for design review |
| 0.2 | YYYY-MM-DD | [@name] | *Example:* Switched worker IDs from static config to leases |
| 0.3 | YYYY-MM-DD | [@name] | *Example:* IDs serialised as strings in all JSON APIs |
| 1.0 | YYYY-MM-DD | [@name] | *Example:* Approved |

## Contents

<!-- Guidance: Keep aligned with the headings. -->

1. [Overview](#1-overview)
2. [Requirements](#2-requirements)
3. [Capacity estimation](#3-capacity-estimation)
4. [API design](#4-api-design)
5. [Data model](#5-data-model)
6. [High-level design](#6-high-level-design)
7. [Detailed design](#7-detailed-design)
8. [Scalability and performance](#8-scalability-and-performance)
9. [Reliability, security and operations](#9-reliability-security-and-operations)
10. [Trade-offs and alternatives](#10-trade-offs-and-alternatives)
11. [Delivery plan and risks](#11-delivery-plan-and-risks)
12. [Appendices](#12-appendices)

## 1. Overview

<!-- Guidance: One page that a busy reviewer can read alone: problem, proposed solution in a paragraph, the decisions that need attention, and the short interview path. -->

### 1.1 Problem statement

[*Example:* Each service uses its own database auto-increment column. After
splitting the orders database into 16 shards, two shards issued the same
order number, and merging data for analytics needs a shard prefix
everywhere. Some teams switched to random UUIDs, which made insert-heavy
tables slower and IDs no longer sort by creation time.]

### 1.2 Proposed solution

[*Example:* Two approved ID formats. Internal high-volume tables use a
64-bit Snowflake-style ID: 41 bits of milliseconds since a custom epoch, 10
bits of worker ID and 12 bits of sequence, generated in-process by a library.
Worker IDs are leased from a coordination store and renewed every 10 s, so
no two live processes share one. Records that are created on devices or
shared across organisations use UUIDv7. All JSON APIs send 64-bit IDs as
strings. A database ticket server remains for the few tables that need dense,
gap-tolerant counters.]

### 1.3 Decisions requested from reviewers

| ID | Question | Recommended option | Section |
|---|---|---|---|
| Q-01 | *Example:* Default format for new tables? | Snowflake-style 64-bit for high-volume internal tables; UUIDv7 elsewhere | 7.1 |
| Q-02 | *Example:* How are worker IDs assigned? | Leases in the coordination store with fencing on expiry | 7.4 |
| Q-03 | *Example:* What happens when the clock goes backwards? | Wait up to 5 ms, then refuse and alert | 7.3 |
| Q-04 | *Example:* Number or string in JSON? | String | 7.6 |
| [Q-nn] | [Question] | [Option] | [Section] |

### 1.4 45-minute interview path

Use this order when the document serves an interview or a short review.

| Minutes | Cover | Output on the whiteboard |
|---|---|---|
| 0 to 5 | Scope: unique, 64-bit or 128-bit, sortable by time, rate; below-the-line items | Four properties |
| 5 to 10 | Estimates: IDs per second, generating processes, lifetime of the layout | Bit budget with a consequence for each field |
| 10 to 15 | Options: auto-increment, ticket server, UUIDv4, UUIDv7, Snowflake | Comparison table |
| 15 to 25 | High-level design: in-process library, lease store, decode tool | One diagram |
| 25 to 40 | One deep dive: clock skew and backwards clocks (default), or worker ID assignment | Options, choice, reason |
| 40 to 45 | Failures, the 53-bit client limit, 10x change | Failure rows |

The interviewer usually wants to see the bit layout with its arithmetic and
a clear answer to "what if the clock goes back". Draw the 64 bits early.

## 2. Requirements

<!-- Guidance: Split core requirements from below-the-line items so the design stays focused. Non-functional targets need numbers and a way to measure them. -->

### 2.1 Core functional requirements

| ID | Requirement | Priority |
|---|---|---|
| FR-01 | *Example:* IDs are unique across all services, regions and time | Must |
| FR-02 | *Example:* IDs fit in a signed 64-bit integer for high-volume tables | Must |
| FR-03 | *Example:* IDs sort roughly by creation time (within a few milliseconds across machines) | Must |
| FR-04 | *Example:* Generation works without a network call on the hot path | Must |
| FR-05 | *Example:* The creation time can be decoded from an ID for debugging | Should |
| FR-06 | *Example:* Records created offline on devices get IDs without a server | Should (UUIDv7) |
| [FR-nn] | [Requirement] | [Must / Should / Could] |

### 2.2 Non-functional requirements

| ID | Quality | Target | Measured as |
|---|---|---|---|
| NFR-01 | Throughput | *Example:* 1 million IDs/s globally at peak; 50 000/s per process | Load test |
| NFR-02 | Latency | *Example:* under 1 microsecond per ID in-process at p99 | Benchmark |
| NFR-03 | Availability | *Example:* Generation continues for 10 minutes if the lease store is down | Chaos test |
| NFR-04 | Uniqueness | *Example:* Zero duplicates, checked by unique constraints and a sampling job | Duplicate detector |
| NFR-05 | Layout lifetime | *Example:* At least 50 years from the custom epoch | Arithmetic in 3.3 |
| [NFR-nn] | [Quality] | [Target] | [Measurement] |

### 2.3 Below the line (out of scope)

- [*Example:* short human-readable codes such as booking references (see URL Shortener System Design, `system-design/design-url-shortener`, for encoding)]
- [*Example:* gap-free invoice numbers required by tax law (a per-series counter in the ledger database)]
- [*Example:* hiding creation time or volume from customers (would need encrypted or random IDs)]
- [*Example:* strict global ordering of events (needs a sequencer or consensus, not an ID scheme)]
- [Item deferred, with the release that will take it]

## 3. Capacity estimation

<!-- Guidance: Every number must end in a consequence for the design. Show inputs, formulas and results; round generously and say which way you rounded. -->

### 3.1 Assumptions

| Input | Symbol | Value | Source |
|---|---|---|---|
| Peak IDs per second, all services | $G$ | *Example:* 1 million | Write traffic forecast |
| Processes that generate IDs | $p$ | *Example:* 1 500 | Deployment inventory |
| Peak IDs per second in the busiest process | $g$ | *Example:* 50 000 | Profiling |
| Rows with an ID after 5 years | $N$ | *Example:* 10 billion | Data growth plan |
| Copies of each ID in indexes and foreign keys | $j$ | *Example:* 3 | Schema review |

### 3.2 Sequence bits

With 12 sequence bits a worker issues $2^{12} = 4\,096$ IDs per millisecond,
which is about 4.1 million per second per worker:

$$
\frac{g}{1000} = \frac{50\,000}{1000} = 50 \ \text{IDs per ms in the busiest process} \ll 4\,096
$$

Consequence: the sequence field is far from the limit; we keep 12 bits and
spend nothing on more.

### 3.3 Time bits and layout lifetime

$$
2^{41} \ \text{ms} = 2\,199\,023\,255\,552 \ \text{ms} \approx 69.7 \ \text{years}
$$

Consequence: 41 bits of milliseconds meet NFR-05 only if the epoch is
recent, so we set a custom epoch at the launch date instead of 1970, which
would already have used nearly 57 years.

### 3.4 Worker bits

10 worker bits give $2^{10} = 1\,024$ worker IDs, but $p = 1\,500$ processes.

Consequence: we cannot give every process a permanent worker ID. Either IDs
are leased only while a process runs and only to processes that generate IDs
(7.4), or the layout changes (for example 12 worker bits and 10 sequence
bits). We choose leases and audit that live generators stay below *Example*
800.

### 3.5 Storage cost of 64-bit versus 128-bit

$$
\Delta = N \times j \times 8 \ \text{B} = 10^{10} \times 3 \times 8 \ \text{B} = 240 \ \text{GB}
$$

Consequence: 128-bit IDs would add about 240 GB of index and key storage
(before replication) on the high-volume tables, plus larger indexes in
memory. 64-bit stays the default for those tables; UUIDv7 is fine elsewhere.

### 3.6 Summary

| Quantity | Estimate | Design consequence |
|---|---|---|
| Per-worker rate | 50 per ms vs 4 096 per ms limit | 12 sequence bits are plenty |
| Layout lifetime | ~69.7 years from epoch | Custom epoch at launch |
| Worker IDs | 1 500 processes vs 1 024 IDs | Leased worker IDs, generators only |
| Width | 240 GB saved at 10 billion rows | 64-bit for high-volume tables |
| JSON safety | IDs exceed $2^{53}$ after about 25 days from epoch (7.6) | IDs as strings in JSON |

## 4. API design

<!-- Guidance: Define the contract first: resources, methods, request and response shapes, errors, idempotency, pagination, rate limits and versioning. Use RFC 9457 problem details for errors. -->

### 4.1 Interfaces

| Interface | Operation | Purpose |
|---|---|---|
| Library | `next_id() -> int64` | Generate one Snowflake-style ID in-process |
| Library | `next_uuid7() -> UUID` | Generate a UUIDv7 |
| Library | `decode(id) -> {timestamp, worker, sequence}` | Debugging and support |
| ID service (for languages without the library) | `POST /v1/ids?count=100` | Batch of IDs over the network |
| Lease store | `acquire`, `renew`, `release` on a worker ID | Worker ID assignment |

### 4.2 Request and response examples

A batch request to the ID service, and a decode result:

```http
POST /v1/ids?count=3 HTTP/1.1
```

```json
{"ids": ["173946227268390913", "173946227268390914", "173946227268390915"], "format": "snowflake64", "epoch": "[YYYY-MM-DDTHH:MM:SSZ]"}
```

```json
{"id": "173946227268390913", "timestamp_ms_since_epoch": 41472012345, "worker_id": 417, "sequence": 1}
```

The decoded example is about 480 days after the epoch, worker 417,
sequence 1; recompute it with your own epoch. The
IDs are strings so JavaScript clients do not round them (7.6).

### 4.3 Errors

| Case | Status | Body type |
|---|---|---|
| Clock moved backwards beyond tolerance | 503 with `Retry-After` | `problems/clock-regression` |
| No worker lease held | 503 | `problems/no-worker-id` |
| Count above maximum batch | 400 | `problems/batch-too-large` |

## 5. Data model

<!-- Guidance: Choose storage by access pattern, not by habit. Show the ID layouts as data structures and the lease records that keep them unique. -->

### 5.1 Snowflake-style 64-bit layout

| Bits | Width | Field | Notes |
|---|---|---|---|
| 63 | 1 | Sign | Always 0, so the value stays positive in signed 64-bit types |
| 62 to 22 | 41 | Milliseconds since custom epoch | About 69.7 years |
| 21 to 12 | 10 | Worker ID | 1 024 values, leased (7.4) |
| 11 to 0 | 12 | Sequence | 4 096 IDs per millisecond per worker |

$$
\text{id} = (t - t_{epoch}) \times 2^{22} + \text{worker} \times 2^{12} + \text{seq}
$$

### 5.2 UUID version 7 layout (RFC 9562)

| Field | Width | Content |
|---|---|---|
| `unix_ts_ms` | 48 | Milliseconds since the Unix epoch, big-endian |
| `ver` | 4 | Version 7 |
| `rand_a` | 12 | Random bits, or a sub-millisecond fraction or counter for monotonic ordering |
| `var` | 2 | Variant bits |
| `rand_b` | 62 | Random bits, or a counter continued from `rand_a` |

UUIDv4 by contrast holds 122 random bits and no time, so it never sorts by
creation.

### 5.3 Lease records

Leases live in the coordination store; ticket sequences live on the ticket
servers. Neither is on the hot path of `next_id()`.

```mermaid
erDiagram
  WORKER_LEASE {
    int worker_id PK
    string holder
    bigint fencing_token
    datetime expires_at
    bigint last_issued_ms
  }
  GENERATOR_PROCESS {
    string holder PK
    string service
    string host
  }
  TICKET_SEQUENCE {
    string name PK
    bigint next_value
    int increment
    int offset
  }
  GENERATOR_PROCESS ||--o| WORKER_LEASE : holds
```

### 5.4 Access patterns

| Query | Key or index that serves it | Partition key and rationale |
|---|---|---|
| Acquire a free worker ID | Scan for `expires_at` in the past, compare-and-set | Single small keyspace in the coordination store; 1 024 rows need no partitioning |
| Renew my lease | `worker_id` with my `fencing_token` | Same keyspace; conditional write fails if someone else took it |
| Resume safely after restart | `last_issued_ms` for the worker | Stored with the lease so a new holder starts after the old holder's last time |
| Insert a row with a new ID | Primary key B-tree on the ID | Time-ordered IDs append near the right edge of the index |
| Find recent rows | Range on the ID (time prefix) | No separate `created_at` index needed for coarse time ranges |
| Next ticket number | `TICKET_SEQUENCE.name` | One row per sequence on the ticket server |

## 6. High-level design

<!-- Guidance: One diagram that shows every component on the generation path and where state lives. -->

```mermaid
flowchart LR
  svc[Service process] --> lib[ID library]
  lib -->|acquire and renew every 10 s| lease[(Coordination store: worker leases)]
  lib --> clock[Local clock, NTP slewing only]
  other[Service without library] --> idsvc[ID service]
  idsvc --> lib2[ID library]
  lib2 --> lease
  legacy[Dense counters] --> ticket[(Ticket server pair)]
  ops[Support tooling] --> decode[Decode tool]
  mon[Duplicate detector] --> dbs[(Databases with unique keys)]
```

| Component | Responsibility | Scales by |
|---|---|---|
| ID library | Compose IDs, guard the clock, hold the lease | With each process |
| Coordination store | Worker leases with expiry and fencing | Small; three-node consensus cluster |
| ID service | Batches for languages without the library | Stateless instances, each with its own lease |
| Ticket server pair | Dense counters with odd and even offsets | Two database instances |
| Duplicate detector | Sample new keys, alert on any duplicate | Batch job |

## 7. Detailed design

<!-- Guidance: Go deep on the parts that are new, risky or contested. For each deep dive list the options, the choice and the reason. -->

### 7.1 Deep dive: choosing an ID scheme

| Scheme | Size | Sortable by time | Coordination | Main weakness |
|---|---|---|---|---|
| Database auto-increment | 64 | Yes, per database | The database | Single writer; collides across shards |
| Ticket server (auto-increment on dedicated hosts) | 64 | Roughly | A network call per ID or batch | Extra dependency on the write path |
| UUIDv4 | 128 | No | None | Random inserts spread across the whole index |
| UUIDv7 | 128 | Yes, by millisecond | None | Twice the width of 64-bit |
| Snowflake-style | 64 | Yes, by millisecond | Worker ID assignment | Depends on clocks and unique worker IDs |

**Choice:** Snowflake-style for high-volume internal tables, UUIDv7 for
records created on devices, exported, or merged across systems, ticket
server only for existing dense counters. **Reason:** Snowflake fits 64 bits
and sorts by time without a call on the hot path; UUIDv7 needs no worker
assignment at all, which matters where we do not control the generator.

### 7.2 Deep dive: composing an ID

```mermaid
sequenceDiagram
  participant A as Application
  participant L as ID library
  participant C as Clock
  A->>L: next_id()
  L->>C: now_ms
  alt now_ms > last_ms
    L->>L: seq = 0, last_ms = now_ms
  else now_ms == last_ms
    L->>L: seq = seq + 1
    opt seq overflows 4095
      L->>C: wait for the next millisecond
    end
  else now_ms < last_ms (clock went back)
    L->>L: apply rule in 7.3
  end
  L-->>A: (last_ms - epoch) shifted 22, OR worker shifted 12, OR seq
```

The library keeps `last_ms` and `seq` per worker under a lock or an atomic
compare-and-set. Starting the sequence at a random small value each
millisecond, instead of 0, spreads low bits for systems that shard by
`id mod n`.

### 7.3 Deep dive: clock skew and clocks moving backwards

Clocks on different machines disagree by milliseconds, and a clock can step
backwards when NTP corrects it or a virtual machine migrates. Disagreement
between machines only blurs ordering; a backwards step on one machine can
produce a duplicate if the same worker reuses old timestamps.

| Option | Behaviour | Risk |
|---|---|---|
| Ignore | Keep generating with the earlier time | Duplicates when the sequence repeats |
| Wait | Sleep until the clock passes `last_ms` | Stalls for the size of the step |
| Refuse | Return an error until the clock catches up | Write errors during the step |
| Logical clock | Keep using `last_ms` and advance it artificially | Time in the ID drifts from real time |

**Choice:** wait if the step is at most *Example* 5 ms; refuse and alert if
larger. Configure NTP to slew (adjust gradually) rather than step, and
persist `last_issued_ms` with the lease so a restarted process with a slow
clock does not reuse time. **Reason:** small steps are common and waiting
is invisible to users; large steps signal a broken host, which should stop
generating rather than risk duplicates.

Ordering across machines is only as good as clock agreement. IDs are
"roughly sorted": if two IDs are created within the skew window on
different machines, their order may not match real time. Do not use ID
order to decide which of two concurrent writes happened first.

### 7.4 Deep dive: worker ID assignment

| Option | How | Risk |
|---|---|---|
| Static config per host | Operator assigns numbers | Copy-paste duplicates, does not fit autoscaling |
| Derived from IP or host name | Hash to 10 bits | Collisions are likely with hundreds of hosts |
| Lease from a coordination store | Compare-and-set a free worker ID with expiry and fencing token | Store outage blocks new processes |
| Central ID service only | Few service instances hold IDs | Network call on the hot path |

**Choice:** leases. A process acquires a worker ID with an expiry of
*Example* 15 minutes and renews every 10 s. If renewal fails, it keeps
generating until 30 s before expiry, then stops, so a lease store outage
leaves about 14 minutes of generation and meets NFR-03 (10 minutes). The
cost is that a worker ID held by a crashed process stays unusable for up to
15 minutes; clean shutdowns release it at once, and the pool keeps live
generators below *Example* 800 of 1 024 for this reason. A new holder of the same worker ID starts only
after the previous lease expired and only at a time later than the stored
`last_issued_ms`. **Reason:** it fits autoscaling and makes duplicate worker
IDs impossible as long as clocks advance, which 7.3 already guards.

### 7.5 Sortability and database indexes

Time-ordered IDs insert near the end of a B-tree primary key, so recent
pages stay in memory and page splits stay rare. Random UUIDv4 keys land
anywhere in the index, touching cold pages and fragmenting it, which slows
insert-heavy tables. Time-ordered keys do create a hot spot on the last
page in some distributed databases that range-partition by key; in those,
hash-partition the table or put a shard prefix in front of the key.

### 7.6 The 53-bit JavaScript limit

JavaScript numbers are double-precision floats and represent integers
exactly only up to $2^{53} - 1$ (`Number.MAX_SAFE_INTEGER`). A
Snowflake-style ID exceeds that once the time part exceeds $2^{31}$ ms,
because $2^{31} \times 2^{22} = 2^{53}$; $2^{31}$ ms is about 24.9 days
after the epoch. After that, `JSON.parse` silently rounds IDs and two
different IDs can compare equal.

- Send 64-bit IDs as JSON strings in every API, and keep them strings in
  client code.
- If a numeric field must stay for compatibility, add a string field next to
  it and document that clients must use the string.
- In JavaScript code that must do arithmetic on IDs, use `BigInt`.

### 7.7 Ticket servers

A ticket server is a database whose only job is to hand out numbers from an
auto-increment column. Two servers with the same increment of 2 and offsets
1 and 2 give odd and even numbers, so either can serve if the other fails.
Clients may fetch blocks of numbers to cut calls. It is simple and produces
compact numbers, but IDs are only roughly ordered between the two servers,
and every block fetch is a network call. Keep it for existing counters.

### 7.8 How real systems do it

> **How real systems do it** (paraphrased, not copied). Twitter announced
> Snowflake in 2010 when moving storage away from a database that could
> issue sequential IDs. The archived project README describes the layout
> used in 5.1: a millisecond timestamp from a custom epoch, a machine number
> configured per host, and a per-machine sequence, with NTP recommended in a
> mode that never steps the clock backwards and a generator that refuses to
> issue IDs until time passes the last one it used. Flickr's 2010 post on
> ticket servers describes the odd and even database pair in 7.7. Instagram's
> 2011 post on sharding and IDs describes generating time-ordered 64-bit IDs
> inside each logical database shard, combining time, shard number and a
> sequence. Read the originals in 12.3; this box keeps only the general idea.

## 8. Scalability and performance

<!-- Guidance: Show how the design meets the numbers in section 3 and where it will break first. -->

| Concern | Approach | Limit before redesign |
|---|---|---|
| Per-process rate | In-process generation with an atomic | 4 096 per ms per worker; add a second worker ID per process beyond that |
| Global rate | No shared state on the hot path | None from the generator; worker IDs are the limit |
| Worker IDs | Leases for generating processes only | 1 024 live generators |
| Lease store | Small, renewals every 10 s | *Example:* 1 024 renewals per 10 s, trivial load |
| ID service | Batches of up to 1 000 | Network latency per batch |

## 9. Reliability, security and operations

<!-- Guidance: Name the failure modes with their blast radius and recovery time, the threats, and the signals operators watch. -->

### 9.1 Failure modes

| Component | Failure | Detection | Blast radius | Mitigation | RTO |
|---|---|---|---|---|---|
| Host clock | Steps backwards | Library metric `clock_regression_ms` | One process | Wait up to 5 ms; refuse beyond; slew-only NTP | *Example:* seconds |
| Host clock | Runs far ahead, then corrected | Time vs. lease store time | IDs from that host sort in the future | Compare with store time at acquire; refuse if skew above 1 s | *Example:* minutes |
| Coordination store | Unavailable | Renewal errors | New processes cannot start generating | Existing leases keep working until expiry; restore quorum | *Example:* 10 min |
| Worker leases | Exhausted (1 024 in use) | Free lease count | New processes cannot generate | Alert at 80%; release on shutdown; reduce generator count | *Example:* 30 min |
| Configuration | Wrong epoch deployed | Decode check in CI and canary | Duplicate or out-of-order IDs | Epoch is a compiled constant with a test | *Example:* rollback |
| Ticket server | One of the pair down | Health check | Half the counter capacity | Clients use the other server | *Example:* immediate |
| [Component] | [Failure] | [Signal] | [Scope] | [Mitigation] | [Time] |

### 9.2 Security and privacy

| Concern | Control |
|---|---|
| IDs reveal creation time | Accept for internal IDs; do not use time-ordered IDs where creation time is sensitive |
| Sequential numbers reveal volume (orders per day) | Do not expose ticket-server numbers publicly; use Snowflake or UUIDv7 in URLs |
| IDs guessed to access other records | Authorisation on every read; an ID is never a secret or a capability |
| UUIDv4 or v7 with weak randomness | Use the platform's cryptographically secure generator |
| [Concern] | [Control] |

### 9.3 Observability

| Signal | Metric | Alert threshold |
|---|---|---|
| Clock regression | Count and size per host | Any above 5 ms |
| Sequence overflow waits | Waits per minute | Sustained above 0 in one process |
| Lease health | Free worker IDs, renewal failures | Free below 20%; any failed renewal streak |
| Duplicates | Unique-constraint violations on ID keys | Any |

## 10. Trade-offs and alternatives

<!-- Guidance: For each major decision list the options, what each costs and why you chose one. Include what changes at ten times and at one tenth of the load. -->

| Decision | Options | Chosen | Why | What we give up |
|---|---|---|---|---|
| Default format | Auto-increment; ticket server; UUIDv4; UUIDv7; Snowflake | Snowflake for high-volume, UUIDv7 elsewhere | 64-bit and sortable, or zero coordination | Two formats to support |
| Worker IDs | Static; derived; leased; central service | Leased | Autoscaling safe | Depends on a coordination store |
| Backwards clock | Ignore; wait; refuse; logical clock | Wait then refuse | No duplicates | Rare write errors |
| Wire format | Number; string | String | Safe in JavaScript | Clients must not do arithmetic on IDs |
| Bit split | 41/10/12; custom | 41/10/12 | Known, lifetime fits | 1 024 worker limit |
| What changes at 10x / 0.1x | 10x processes: worker bits run out first, so move to 12 worker bits or route generation through an ID service. 0.1x: UUIDv7 everywhere and no lease store | Review with each platform change | Process count, not ID rate, drives the design | [Note] |
| [Decision] | [Options] | [Chosen] | [Why] | [Cost] |

## 11. Delivery plan and risks

<!-- Guidance: How the design reaches production safely, and what could stop it. -->

### 11.1 Milestones

| Milestone | Scope | Exit criterion | Target date |
|---|---|---|---|
| M1 | Library with leases, clock guard, decode tool | Property tests and chaos test with clock steps pass | YYYY-MM-DD |
| M2 | New tables use the library; APIs send strings | Contract tests in all clients | YYYY-MM-DD |
| M3 | ID service for other languages | Batch p99 under 5 ms | YYYY-MM-DD |
| M4 | Duplicate detector and dashboards | Zero duplicates over 30 days | YYYY-MM-DD |

### 11.2 Rollout and rollback

[*Example:* Existing tables keep their IDs; only new tables and new rows in
migrated tables use the new format, chosen so new IDs are larger than all
existing ones. Rollback stops the library from being used for new tables; IDs
already issued remain valid forever.]

### 11.3 Risks

| ID | Risk | Likelihood | Impact | Mitigation | Owner |
|---|---|---|---|---|---|
| R-01 | *Example:* A client parses IDs as numbers and corrupts them | Medium | High | Strings only, contract tests, lint rule | [@name] |
| R-02 | *Example:* Worker IDs exhausted after autoscaling growth | Medium | Medium | Alerts at 80%, layout change plan ready | [@name] |
| R-03 | *Example:* NTP misconfigured to step clocks | Low | High | Host baseline check, clock guard in the library | [@name] |
| [R-nn] | [Risk] | [L / M / H] | [L / M / H] | [Mitigation] | [@name] |

## 12. Appendices

<!-- Guidance: Supporting detail, glossary, references and open questions. -->

### 12.1 Open questions

| ID | Question | Owner | Due |
|---|---|---|---|
| OQ-01 | *Example:* Should the worker field split into region and worker bits? | [@name] | YYYY-MM-DD |
| OQ-02 | *Example:* Which UUIDv7 monotonic method does our library implement? | [@name] | YYYY-MM-DD |
| [OQ-nn] | [Question] | [@name] | YYYY-MM-DD |

### 12.2 Glossary

| Term | Meaning |
|---|---|
| Custom epoch | The moment from which ID timestamps count, chosen near launch |
| Worker ID | Number that makes concurrent generators produce disjoint IDs |
| Lease | Time-limited exclusive right to a worker ID |
| Fencing token | Increasing number that lets the store reject an older holder |
| Roughly sorted (k-sorted) | Sorted by time except within the clock skew window |
| Ticket server | Database host that only issues auto-increment numbers |

### 12.3 References

| Reference | Use |
|---|---|
| RFC 9562, "Universally Unique IDentifiers (UUIDs)", IETF, May 2024 (obsoletes RFC 4122) | 5.2 UUIDv4 and UUIDv7 layouts |
| Twitter engineering blog, "Announcing Snowflake", June 2010, and the archived `twitter-archive/snowflake` README on GitHub | 5.1, 7.3, 7.8 |
| Flickr engineering blog, "Ticket Servers: Distributed Unique Primary Keys on the Cheap", 8 February 2010 | 7.7, 7.8 |
| Instagram engineering blog, "Sharding & IDs at Instagram", 2011 | 7.8 |
| MDN Web Docs, `Number.MAX_SAFE_INTEGER`, and ECMA-262 | 7.6 |
| Xu, *System Design Interview: An Insider's Guide* (volume 1, 2020), chapter "Design a Unique ID Generator in Distributed Systems" | Inspiration only; no text or figures reused |
| URL Shortener System Design (`system-design/design-url-shortener`) | Short codes from IDs |
| Distributed Key-Value Store System Design (`system-design/design-distributed-kv-store`) | Partitioning by key |
| Back-of-Envelope Estimation and Capacity Model (`system-design/back-of-envelope-estimation`) | Method for section 3 |

## Standard and licence note

Based on RFC 9562 for UUID formats and on common industry practice for
Snowflake-style identifiers, using the public sources cited in 12.3. The
book and engineering write-ups listed there are inspiration only; no text,
diagrams or numbers were copied, and all figures here are illustrative
examples. Check the current RFC errata and your language's UUID library
before relying on a specific version or monotonic method. Original text
written for this collection. Template licence: MIT.
