$npx -y skills add proyecto26/system-design-skills --skill sequencerThis skill should be used when the user needs a "unique ID generator", "distributed IDs", a "Snowflake ID", asks "UUID vs auto-increment", wants a "time-sortable ID", a "monotonic sequence", a "ticket server", or "ID generation at scale". It gives a menu of ID schemes (UUID/ULID,
| 1 | # Sequencer |
| 2 | |
| 3 | Hand out identifiers that are unique across every node without a central |
| 4 | bottleneck — and decide whether those IDs must also be *sortable* or *monotonic*. |
| 5 | Getting this wrong shows up late and hard: collisions corrupt data, a single |
| 6 | allocator caps write throughput, and IDs that leak a creation time or a sequential |
| 7 | count expose business secrets and enable enumeration attacks. |
| 8 | |
| 9 | ## When to reach for this |
| 10 | A system writes new records across multiple nodes and each needs a primary key |
| 11 | (orders, messages, uploads, events). Reach for this when a single auto-increment |
| 12 | column would serialize all writes, when IDs must be generated before a DB round |
| 13 | trip (client-side, offline), or when records must be roughly time-ordered without |
| 14 | a separate sort field. |
| 15 | |
| 16 | ## When NOT to |
| 17 | A single relational node still comfortably serves the write load (→ |
| 18 | `back-of-the-envelope`) — then a plain `BIGINT AUTO_INCREMENT`/`SERIAL` is the |
| 19 | cheapest correct answer; do not build a distributed ID service for it (YAGNI). |
| 20 | If a natural unique key already exists (email, ISBN, content hash), use it. Don't |
| 21 | demand global monotonicity unless an invariant truly needs it — it is the most |
| 22 | expensive property here and usually only *per-entity* ordering is required. |
| 23 | |
| 24 | ## Clarify first |
| 25 | - **Generation point** — client/edge, app server, or database? (Decides whether a |
| 26 | DB round trip per ID is acceptable.) |
| 27 | - **Ordering need** — none, *time-sortable* (k-sorted is fine), or *strictly |
| 28 | monotonic*? Per-entity or global? This is the single biggest fork. |
| 29 | - **Write rate & node count** — IDs/sec at peak and how many generators (→ |
| 30 | `back-of-the-envelope`). Sets the bits needed for a sequence counter. |
| 31 | - **Size & encoding budget** — 64-bit int (fits an indexed key cheaply) vs 128-bit |
| 32 | (no coordination ever) vs short URL-safe string? |
| 33 | - **Leakage tolerance** — may the ID reveal creation time or a guessable count |
| 34 | (enumeration / competitor signal)? |
| 35 | |
| 36 | ## The options |
| 37 | - **Auto-increment / SQL sequence** — one DB column hands out IDs. Use when a |
| 38 | single node owns the writes and you want zero new infrastructure. |
| 39 | - **UUIDv4 (random 128-bit)** — generate anywhere, no coordination, effectively |
| 40 | zero collision risk. Use when you only need uniqueness and never sort by ID. |
| 41 | - **ULID / UUIDv7 (time-prefixed 128-bit)** — random but with a millisecond |
| 42 | timestamp prefix, so IDs sort by creation time. Use when you want UUIDv4's |
| 43 | zero-coordination *and* time-ordering (the modern default for new keys). |
| 44 | - **Snowflake-style (timestamp + node + sequence, 64-bit)** — pack a timestamp, |
| 45 | a node ID, and a per-ms counter into a sortable 64-bit int. Use at high write |
| 46 | rates where a compact, k-sorted integer key matters. |
| 47 | - **DB ticket / range allocation (Flickr-style)** — a central table hands out |
| 48 | *blocks* of IDs (e.g. 1000 at a time); each node serves from its block in |
| 49 | memory. Use when you want simple monotonic-ish integers without per-ID |
| 50 | coordination. |
| 51 | |
| 52 | ## Trade-offs |
| 53 | |
| 54 | | Option | What it solves | What it worsens | Change it when | |
| 55 | |---|---|---|---| |
| 56 | | Auto-increment / sequence | Trivial, monotonic, compact int | Serializes writes; single node caps throughput; leaks count | Writes outgrow one node, or you need client-side IDs → ticket/Snowflake | |
| 57 | | UUIDv4 (random) | Generate anywhere, no coordination, no leakage | 128-bit; random order kills index locality (page splits); not sortable | You need time-ordering → ULID/UUIDv7 | |
| 58 | | ULID / UUIDv7 | Zero coordination + time-sortable + index-friendly | Still 128-bit; only ms-sortable (not strict); leaks creation time | You need a 64-bit key or strict order → Snowflake / sequence | |
| 59 | | Snowflake-style (64-bit) | Compact, k-sorted, ~4M IDs/node/sec | Needs node-ID assignment + clock-skew handling; epoch/bit budget caps lifespan | Clock sync is unreliable, or you can't assign node IDs → ULID | |
| 60 | | DB ticket / range | Monotonic-ish ints, low coordination, simple | Allocator table is a SPOF; gaps on restart; only loosely ordered across nodes | Allocator becomes a bottleneck or SPOF → Snowflake/ULID | |
| 61 | |
| 62 | ## Behavior under stress |
| 63 | The whole point of distributed ID schemes is to avoid a single allocator, so the |
| 64 | failure modes cluster around *coordination shortcuts*. |
| 65 | |
| 66 | - **Allocator as SPOF/bottleneck (ticket, single sequence):** every write blocks on |
| 67 | one row/node. A spike or its failure stalls all inserts. *Mitigate:* hand out |
| 68 | larger ranges, replicate the allocator, or move to Snowflake/ULID (no central |
| 69 | hop). Larger ranges trade away |