Skip to content
100% in your browser. Nothing you paste is uploaded — all processing runs locally. Read more →

Why Stripe, Discord, and Shopify don't use UUIDs

9 min read #uuid #api-design #ids #stripe #discord #shopify

On this page
  1. Stripe: prefixed IDs (cus_..., ch_..., evt_...)
    1. Why prefixes?
    2. What’s the cost?
  2. Discord: Snowflake IDs
    1. Why Snowflakes?
    2. What’s the cost?
  3. Shopify: GIDs (Global IDs)
    1. Why Global IDs?
    2. What’s the cost?
  4. Three different problems
  5. When you should deviate
  6. Try the tool
  7. Further reading

Open the developer console for almost any modern API and you’ll find one of two things: a UUID, or a deliberate choice not to use one. The deliberate choices are interesting — three of the largest API providers on the web use systems they built themselves, and the design rationale behind each is worth understanding.

This post walks through Stripe’s prefixed IDs, Discord’s Snowflakes, and Shopify’s Global IDs. None of them is “better than UUIDs” in the abstract — but each one solves problems UUIDs don’t, and each one ignores problems UUIDs do solve.

Stripe: prefixed IDs (cus_..., ch_..., evt_...)

Stripe’s identifiers look like this:

cus_NffrFeUfNV2Hib
ch_3OhrzS2eZvKYlo2C0xAZGXKx
evt_3OhrzS2eZvKYlo2C0xAZGXKx
in_1OhrzS2eZvKYlo2C0xAZGXKx
sub_1OhrzS2eZvKYlo2C0xAZGXKx

Two parts: a type prefix that identifies the resource (cus_ for customer, ch_ for charge, evt_ for event, in_ for invoice, sub_ for subscription, pi_ for payment intent, etc.) followed by a random base62-ish string.

This isn’t a UUID. The format looks like KSUID or a similar ULID-style identifier underneath, but the wrapper is what matters.

Why prefixes?

Stripe ships SDKs in 8+ languages with thousands of developer-facing endpoints. Their API ergonomic priority is the developer can read an ID and immediately know what it is. Concrete benefits:

  1. Self-documenting logs. A line Refund failed: ch_3OhrzS2… tells you the failed thing was a charge. With a UUID, you’d need to look it up.

  2. Fail-fast on misuse. If a Stripe SDK method expects a customer ID and you pass pi_… (payment intent), the SDK can reject it client-side without a round-trip. UUIDs can’t.

  3. No lookups for type detection. Webhook payloads are JSON; parsing event.data.object.id and routing by prefix is a couple of characters of code instead of a database join.

  4. Forgiving copy-paste. If a developer accidentally pastes a payment-intent ID into a “subscription ID” field, the system can say “that looks like a payment intent, did you mean…” instead of a 404.

What’s the cost?

For Stripe’s audience (developers building integrations), the self-documentation wins. Their support tickets are a mountain of “I sent the wrong ID type” — anything that prevents that mistake at the client SDK layer pays for itself.

If you want this pattern: KSUID or ULID with a type prefix in front gets you most of the way there. We have a comparison of UUID v7 vs ULID here.

Discord: Snowflake IDs

Discord’s IDs look very different:

1078123456789012345
1234567890123456789

Plain integers. Specifically: 64-bit integers following a format called Snowflake, originally invented at Twitter (now X) in 2010.

The 64 bits are split:

┌─────────────────────────────────────────────────────────────┐
│ 1-bit │ 41-bit ms timestamp │ 5-bit datacenter │ 5-bit       │
│ sign  │ (since 2015 epoch)  │ ID               │ worker ID   │
│ (0)   │                     │                   │             │
│       │                     │                   │             │
│       │                     │                   │             │
│       │                     │                   ▼             │
│       │                     │                   12-bit        │
│       │                     │                   sequence      │
└─────────────────────────────────────────────────────────────┘

Discord’s official documentation publishes this layout. Given a Discord ID, you can decode the exact millisecond it was created, which Discord datacenter generated it, and which worker process within that datacenter.

Why Snowflakes?

Discord’s calculus is different from Stripe’s. They aren’t selling APIs to integrators — they’re running a real-time chat platform with hundreds of millions of users.

  1. Compact storage. 8 bytes vs 16 (UUID v7). At Discord’s scale (billions of messages per day, Cassandra cluster storing trillions of rows) the 2× difference compounds.

  2. Time-ordered out of the box. A Snowflake’s high bits are a timestamp. Sort numerically → sort chronologically. Database indexes (especially Cassandra’s wide-row partitioning) become simpler.

  3. Embedded provenance. “Which datacenter handled this message?” is a 5-bit lookup, not a join.

  4. Public timestamp visibility. Users see message IDs in URLs (https://discord.com/channels/<server>/<channel>/<message>) and tools can derive the message’s creation time without an API call. A nice-to-have for moderation, search, and embedded timestamps.

What’s the cost?

Three real costs:

Compare to UUID v7, which:

UUID v7 was published as RFC 9562 in May 2024. If Discord were designing today, they’d plausibly pick v7 — they’d lose the datacenter-ID feature but gain coordination-free generation. They won’t migrate; rewriting trillions of message IDs costs more than any benefit.

Shopify: GIDs (Global IDs)

Shopify’s REST API uses simple incrementing integers (123456789), but their newer GraphQL API uses Global IDs:

gid://shopify/Product/108828309
gid://shopify/Order/450789469
gid://shopify/Customer/207119551

Three parts joined: the literal prefix gid://shopify/, the resource type, and the underlying integer ID.

Why Global IDs?

Shopify (documented at length) chose this for the GraphQL Relay specification. Relay requires every object in a GraphQL graph to be addressable by a single opaque global ID. The format Shopify picked is functional:

  1. Globally unique across resource types. A product ID 100 and a customer ID 100 produce different GIDs (gid://shopify/Product/100 vs gid://shopify/Customer/100).

  2. Self-documenting. Like Stripe, a GID tells you what it is at a glance.

  3. Compatible with REST IDs. The numeric portion is the same as the REST API, so apps can convert one to the other without a lookup. Migrating an old REST integration to GraphQL is mostly a sed expression.

  4. GraphQL Relay-ready. Relay’s caching, pagination, and refetch semantics all assume a single global ID type.

What’s the cost?

The GID pattern is a layer on top of whatever underlying ID system Shopify uses. If the underlying integers ever need to change (say, to UUID v7 for a new database), the GID wrapper insulates external clients from the change. That’s worth a lot.

Three different problems

Each company solved a different problem:

CompanyUnderlying problemSolutionCost they accepted
StripeDeveloper mistakes mixing up resource typesPrefixed IDs with type metadataFormat lock-in; can’t generate outside Stripe
DiscordCompact storage at petabyte scale64-bit Snowflake with embedded time + provenanceWorker-ID coordination; year-2080 ceiling
ShopifyGraphQL Relay needs single global ID typeURI-style wrapper around their existing REST integersVerbose strings; REST/GraphQL translation cost

UUID v7 is the boring correct answer for most new systems. Each of these three companies had a specific reason to deviate, and the deviation was deliberate, well-documented, and worth the cost. None of them ignored UUIDs out of NIH; they’re all aware of the trade-off.

When you should deviate

Default to UUID v7 (or v4 if you don’t need ordering). Deviate only when you can name the specific cost you’re paying:

For everyone else: UUID v7 is the right default. We have a longer discussion of the trade-offs in UUID as primary key and the v7 vs ULID comparison.

Try the tool

If you want to inspect a UUID and see what it embeds, paste it into the validator. For generating v7 IDs, see the v7 generator. And for a feel of what a hundred or a thousand v7 UUIDs look like in time order, the bulk generator generates up to 10,000 at a time.

Further reading