Stop Using Raw UUIDs: Type-Safe, Prefixed IDs in Rust (Stripe-style)

Published: (December 24, 2025 at 12:46 AM EST)
2 min read
Source: Dev.to

Source: Dev.to

We have all been there. You are staring at your server logs at 2 AM, trying to debug a request, and you see this:

Processing request for ID: 550e8400-e29b-41d4-a716-446655440000

Is that a User ID? An Order ID? An API Key? Who knows—just a blob of hex characters.

Even worse, you might write a function like this:

fn process_payment(user_id: Uuid, order_id: Uuid) { /* … */ }

If you accidentally call process_payment(order_id, user_id), the compiler won’t stop you. Both arguments are just Uuid, so you won’t discover the mistake until the database throws a “Record not found” error—or worse, you corrupt data.

A Stripe‑style solution

Stripe’s IDs are self‑describing (cus_018…, ch_018…). They are readable and type‑safe.
I wanted the same experience in Rust, but with compile‑time safety. The result is puuid.

puuid (Prefixed UUID) is a lightweight crate that wraps the standard uuid library and gives you:

  • Readability – IDs print as "user_018c…" instead of just numbers.
  • Type safety – You cannot mix up a UserId with an OrderId.

Defining prefixes

use puuid::{Puuid, prefix};

// 1. Define your prefixes
prefix!(User, "user");
prefix!(Order, "ord");

// 2. Create strong types
pub type UserId = Puuid;
pub type OrderId = Puuid;

Compile‑time protection

fn delete_order(id: OrderId) {
    println!("Deleting order: {}", id);
}

fn main() {
    let user_id = UserId::new_v7();

    // ❌ COMPILE ERROR: expected `OrderId`, found `UserId`
    delete_order(user_id);
}

The Rust compiler now prevents accidental swaps.

Implementation details

  • Puuid is a #[repr(transparent)] wrapper around uuid::Uuid.
  • Memory – Exactly the same size as a standard UUID (16 bytes).
  • Database – Insert directly as a UUID (.into_inner()) or store as text to keep the prefix visible.
  • Methods – Implements Deref, so you can still call .as_bytes(), .get_version(), etc., from the uuid crate.

UUID v7 by default

If you haven’t switched from v4 (random) to v7 yet, you should. v7 UUIDs are time‑sortable, which means they naturally index by creation time in a database, reducing index fragmentation and speeding up large‑table inserts.

let id = UserId::new_v7();
// Output: user_018c6427-4f30-7f89-a1b2-c3d4e5f67890

JSON API integration

When building a JSON API (Axum, Actix, etc.), puuid handles (de)serialization automatically:

#[derive(Serialize, Deserialize)]
struct Checkout {
    id: OrderId,
    customer: UserId,
}

Client JSON

{
  "id": "ord_018...",
  "customer": "user_018..."
}

If a client sends a raw UUID or mixes up the prefixes, deserialization fails with a validation error.

  • Crates.io:
  • Docs:
  • GitHub:

Feel free to share your thoughts on the API design! 🦀

Back to Blog

Related posts

Read more »

Data vs Behavior in Rust

Structs in Rust: Modeling Data In Rust, a struct is used purely to represent data. rust struct Rect { width: f32, height: f32, } A struct does not define behav...