typedflake

Crates.iotypedflake
lib.rstypedflake
version0.1.3
created_at2025-10-09 16:09:25.261105+00
updated_at2025-10-10 17:20:22.988667+00
descriptionA Snowflake-style ID generator library with newtype-driven design
homepagehttps://github.com/pokedotdev/typedflake
repositoryhttps://github.com/pokedotdev/typedflake
max_upload_size
id1875835
size149,408
Poke (pokedotdev)

documentation

https://docs.rs/typedflake

README

TypedFlake

Distributed, type-safe Snowflake ID generation for Rust.

Generate unique, time-ordered 64-bit IDs across distributed systems without coordination. Inspired by Twitter's Snowflake algorithm, with strong type safety through Rust's newtype pattern.

Features

  • 🌍 Distributed: Multi-server support via worker/process IDs
  • πŸ”’ Thread-safe: Lock-free atomic operations using compare-and-swap
  • ⏱️ Time-ordered: IDs are sortable by creation time
  • πŸ›‘οΈ Type-safe: Each ID type is distinct (no mixing UserId with OrderId)
  • πŸŽ›οΈ Customizable: Configure bit allocation and epoch per-type or globally
  • πŸ’Ύ Shared state pool: Generators share lazy state per (worker, process) pair
  • 🏭 Battle-tested: Industry-standard presets from Twitter and Discord
  • πŸ“¦ Serde support: Optional JSON serialization as strings (IEEE 754 safe)

Quick Start

// Define ID types
typedflake::id!(UserId);
typedflake::id!(OrderId);

fn main() {
    // Generate IDs (thread-safe)
    let user_id = UserId::generate();
    let order_id = OrderId::generate();

    println!("Order: {order_id}");
    println!("User: {user_id}");

    // Access components by tuple
    let (timestamp, worker_id, process_id, sequence) = user_id.decompose();
}

Basic Generation

Generate IDs using the default instance (worker=0, process=0):

typedflake::id!(UserId);

let id = UserId::generate();

Multiple ID types are completely independent:

typedflake::id!(UserId);
typedflake::id!(OrderId);

let user_id = UserId::generate();
let order_id = OrderId::generate();

// βœ… Type-safe: These cannot be accidentally mixed
fn process_user(id: UserId) { }
process_user(order_id); // ❌ Compile error!

Instance-Based Generation

Create generators bound to specific worker/process IDs for distributed systems:

typedflake::id!(UserId);

// Server-based: worker ID represents physical/virtual server
let server_15 = UserId::worker(15)?;
let id = server_15.generate();

// Process-based: process ID for multi-process applications
let process_7 = UserId::process(7)?;
let id = process_7.generate();

// Full control: assign both worker and process IDs
// Example: worker=region, process=datacenter
let us_east_dc2 = UserId::instance(31, 15)?;
let id = us_east_dc2.generate();

[!NOTE] State Sharing: Generator instances for the same (worker_id, process_id) pair share the same underlying atomic state. This makes it safe and efficient to create multiple generators for the same IDs across different threads or contextsβ€”they coordinate through shared state without duplication.

[!TIP] For containerized deployments (Kubernetes, Docker), use Global Defaults to configure worker/process IDs from environment variables. This eliminates the need to pass IDs throughout your application.

Configuration

Presets

Use battle-tested configurations:

use typedflake::{BitLayout, Config, Epoch};

// Config presets (BitLayout + Epoch)
typedflake::id!(TwitterId, Config::TWITTER);   // 42t|10w|0p|12s, epoch: Nov 2010
typedflake::id!(DiscordId, Config::DISCORD);   // 42t|5w|5p|12s, epoch: Jan 2015

// BitLayout presets (use with custom epoch)
BitLayout::TWITTER;   // 42t|10w|0p|12s - 1024 workers, 4096 IDs/ms per worker
BitLayout::DISCORD;   // 42t|5w|5p|12s - 1024 instances, 4096 IDs/ms per instance
BitLayout::DEFAULT;   // Same as DISCORD

// Epoch presets
Epoch::TWITTER;    // Nov 4, 2010 01:42:54 UTC
Epoch::DISCORD;    // Jan 1, 2015 00:00:00 UTC
Epoch::DEFAULT;    // Jan 1, 2025 00:00:00 UTC

[!TIP] New projects: Use a custom epoch near your launch date to maximize capacity. See Choosing an Epoch below.

Custom Configuration

use typedflake::{BitLayout, Config, Epoch};

// Create custom bit allocation
const CUSTOM_CONFIG: Config = Config::new_unchecked(
    BitLayout::new(42, 5, 5, 12),     // timestamp, worker, process, sequence
    Epoch::from_date(2025, 9, 13)     // Custom epoch date
);

typedflake::id!(CustomId, CUSTOM_CONFIG);

Choosing an Epoch

Recommended for new projects: Set your epoch near to your project's launch date.

// Recommended: Set epoch near to your actual launch date
const CONFIG: Config = Config::new_unchecked(
    BitLayout::DEFAULT,
    Epoch::from_date(2025, 9, 13) // Your project launch
);

// Suboptimal: Using old preset epochs
const CONFIG: Config = Config::DISCORD;  // Epoch from 2015
// This approach consumes years of timestamp capacity before your project even existed

Why this matters:

  • βœ… Maximizes your timestamp lifespan starting from when you actually need it
  • βœ… Keeps timestamp values smaller during your project's early years
  • βœ… Aligns IDs with your project timeline

[!CAUTION] Only change your epoch if you're absolutely certain no IDs have been generated in production yet. Otherwise, keep your current epochβ€”compatibility with existing IDs is more important than reclaiming unused years.

Global Defaults

In distributed systems (microservices, Kubernetes, multi-region), each service instance typically has the same worker/process ID throughout its lifecycle. Global defaults eliminate the need to pass these IDs aroundβ€”set them once at startup, then use the simple generate() API everywhere.

[!IMPORTANT] Global defaults should be set once at application startup before generating any IDs. They cannot be changed after initialization.

Without global defaults - must create instances:

let generator = UserId::instance(worker_id, process_id)?;
let id = generator.generate(); // Repeat for every service

With global defaults - set once, use everywhere:

use typedflake::Config;

typedflake::id!(UserId);
typedflake::id!(OrderId);

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Read from environment (Kubernetes, Docker, etc.)
    let worker_id = std::env::var("POD_ORDINAL").unwrap_or("0".into()).parse()?;
    let process_id = std::env::var("CONTAINER_ID").unwrap_or("0".into()).parse()?;

    // Set defaults once at startup
    typedflake::global::set_defaults(Config::DISCORD, worker_id, process_id)?;
    // Or set only config/instance
    typedflake::global::set_default_config(Config::DISCORD)?;
    typedflake::global::set_default_instance(worker_id, process_id)?;

    // Simple API throughout your application
    let user_id = UserId::generate();   // Uses defaults
    let order_id = OrderId::generate(); // Uses defaults
    Ok(())
}

Component Access

typedflake::id!(UserId);
let id = UserId::generate();

// Decompose to tuple
let (timestamp, worker_id, process_id, sequence) = id.decompose();

// Components struct
let components = id.components();
println!("{}", components.timestamp);

// Individual accessors
let timestamp = id.timestamp();
let worker = id.worker_id();
let process = id.process_id();
let sequence = id.sequence();

Composition & Conversions

Compose IDs from Components

typedflake::id!(UserId);

// Compose with default worker/process (validated)
let id = UserId::compose(1234567890, 42)?;

// Compose with all components (validated)
let id = UserId::compose_custom(1234567890, 15, 7, 42)?;

// Unchecked variants (masks overflow, better performance)
let id = UserId::compose_unchecked(1234567890, 42);
let id = UserId::compose_custom_unchecked(1234567890, 15, 7, 42);

u64 Conversions

let id = UserId::generate();

// To u64
let raw: u64 = id.as_u64();
let raw: u64 = id.into();

// From u64 (validated - use for external data)
let id = UserId::try_from_u64(raw)?;
let id: UserId = raw.try_into()?;

// From u64 (unchecked - use for trusted sources)
let id = UserId::from_u64_unchecked(raw);

String Conversions

let id = UserId::generate();

// To string
let s = id.to_string();
println!("ID: {s}");

// From string
let parsed: UserId = s.parse()?;
assert_eq!(id, parsed);

JSON Serialization (Serde)

Enable the serde feature for JSON serialization:

[dependencies]
typedflake = { version = "0.1", features = ["serde"] }

IDs serialize as strings (not numbers) for safe cross-language compatibility:

use serde::{Deserialize, Serialize};

typedflake::id!(UserId);

#[derive(Serialize, Deserialize)]
struct User {
    id: UserId,
    name: String,
}

let user = User {
    id: UserId::generate(),
    name: "Alice".to_string(),
};

let json = serde_json::to_string_pretty(&user)?;

JSON output:

{
  "id": "1234567890123456789",
  "name": "Alice"
}

[!TIP] Why strings? JSON numbers are typically parsed as IEEE 754 double-precision floats, which safely represent integers up to 53 bits. Snowflake IDs are 64-bit, so values above 9_007_199_254_740_991 lose precision when parsed as numbers. String serialization ensures safe transmission across languages (JavaScript, Python, Java, Go, etc.) and web APIs without data loss.


Architecture

TypedFlake uses a newtype-driven architecture where each ID type maintains completely independent state:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ typedflake::id!(UserId)                 β”‚
β”‚                                         β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Static IdContext (OnceLock)         β”‚ β”‚
β”‚ β”‚                                     β”‚ β”‚
β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚ β”‚
β”‚ β”‚ β”‚   Config    β”‚  β”‚  StatePool    β”‚  β”‚ β”‚
β”‚ β”‚ β”‚ (BitLayout, β”‚  β”‚  (DashMap)    β”‚  β”‚ β”‚
β”‚ β”‚ β”‚   Epoch)    β”‚  β”‚               β”‚  β”‚ β”‚
β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚ β”‚
β”‚ β”‚                      β”‚              β”‚ β”‚
β”‚ β”‚         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚ β”‚
β”‚ β”‚         β”‚ Lazy State Creation β”‚     β”‚ β”‚
β”‚ β”‚         β”‚ Arc<AtomicU64>      β”‚     β”‚ β”‚
β”‚ β”‚         β”‚ (worker, process)   β”‚     β”‚ β”‚
β”‚ β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key design:

  • Per-type isolation: Each typedflake::id!(TypeName) creates a separate static context
  • Lock-free generation: Atomic compare-and-swap operations on packed u64 state
  • Lazy allocation: States created on-demand per (worker, process) pair using DashMap and shared across all generators for that pair

ID Structure

A TypedFlake ID is a 64-bit integer divided into four components:

Component Bits Range Description
Timestamp 42 0 - 4,398,046,511,103 Milliseconds since epoch
Worker ID 5 0 - 31 Worker identifier
Process ID 5 0 - 31 Process identifier
Sequence 12 0 - 4,095 IDs/ms (per instance)

Default: 42t|5w|5p|12s = 139 years, 1024 instances, 4096 IDs/ms per instance

Total system capacity scales with instances: 1024 instances Γ— 4096 IDs/ms = 4,194,304 IDs/ms

Bit allocation is fully customizable:

BitLayout::new(42, 4, 4, 14);  // High-throughput: 16,384 IDs/ms per instance
BitLayout::new(45, 4, 5, 10);  // Long-lived: ~1,115 years

Performance

TypedFlake is designed for high-throughput scenarios:

  • Lock-free: Atomic compare-and-swap operations with no mutexes
  • Zero allocations: ID generation doesn't allocate memory
  • Cache-friendly: Packed atomic state with cache-line alignment
  • Lazy initialization: Only allocates state for actively-used instances

Run benchmarks:

cargo bench

License

MIT License

Acknowledgments

Algorithm inspirations:

Design philosophy:

Commit count: 0

cargo fmt