Magic Type ID (MTI): Type-Safe, Human-Readable, Unique Identifiers for Rust
Are you looking for a better way to handle identifiers in your Rust applications? Do opaque UUIDs make your logs, databases, and APIs harder to understand? Magic Type ID (MTI) offers a solution.
It provides identifiers that look like user_01h455vb4pex5vsknk084sn02q or order_01h455vb4pex5vsknk084sn02q – combining a human-readable prefix indicating the type of entity with a globally unique, sortable (by default) suffix. This approach, inspired by conventions like Stripe's API IDs, offers immediate clarity.
mti is a Rust crate that provides type-safe, human-readable, and globally unique identifiers. Based on the TypeID Specification, MTI enhances UUIDs with descriptive prefixes. This improves debuggability, code reliability, and simplifies identifier management in various applications.
⭐ If you find MTI useful, please consider starring the repository on GitHub! ⭐
Why Use MTI? Solved Problems & Key Benefits
MTI addresses common challenges developers encounter with traditional identifiers:
- Problem: Opaque and Confusing IDs.
- MTI Solution: Human-readable prefixes (e.g.,
user_,order_) make identifiers instantly recognizable, aiding in debugging and log analysis. You no longer need to guess what an ID likef47ac10b-58cc-4372-a567-0e02b2c3d479refers to.
- MTI Solution: Human-readable prefixes (e.g.,
- Problem: Risk of ID Misuse.
- MTI Solution: Embedding type information directly into the ID helps prevent runtime errors where an ID for one entity (e.g., a
product_id) is mistakenly used for another (e.g., auser_id). This can be further enforced by creating custom newtypes aroundMagicTypeId.
- MTI Solution: Embedding type information directly into the ID helps prevent runtime errors where an ID for one entity (e.g., a
- Problem: Ensuring ID Uniqueness and Integrity.
- MTI Solution: Provides a standardized, globally unique, and self-descriptive ID format that simplifies data integrity across different parts of an application or between systems.
- Problem: Complex ID Generation & Sorting.
- MTI Solution: Offers an intuitive API for generating various UUID versions (including time-sortable UUIDv7 by default), abstracting the complexities of UUID generation and ensuring consistency.
Key Benefits for Developers:
- Enhanced Readability & Debuggability: Instantly understand the type and context of an identifier.
- Improved Type Safety: Reduce the risk of logical errors by making ID types explicit.
- Simplified Development: Easy-to-use API for creating, parsing, and manipulating TypeIDs.
- Global Uniqueness: Leverages the power of UUIDs.
- Inherent Sortability (with UUIDv7): Generate IDs that can be naturally sorted by time, useful for event streams and ordered data.
- Performance-Oriented: Designed with zero-cost abstractions for common string-like operations.
- Specification Adherence: Implements the TypeID Specification for potential interoperability.
- Robust & Reliable: Built on the
TypeIdPrefixandTypeIdSuffixcrates.
Understanding UUID Versions in MTI
MTI supports several UUID versions, each with distinct characteristics and advantages. The choice of UUID version impacts properties like sortability and determinism:
-
UUIDv7 (Time-Ordered - Default for new IDs):
- How it works: Combines a high-precision Unix timestamp (usually milliseconds) with random bits to ensure uniqueness.
- Advantages:
- Sortable by Time: IDs are k-sortable by creation time, which is beneficial for database indexing (improving insert performance for clustered indexes) and for ordering records chronologically (e.g., event logs, audit trails).
- Globally unique.
- Use Cases: Primary keys in databases, event identifiers, any scenario where chronological ordering is valuable.
-
UUIDv4 (Random):
- How it works: Generated from purely random or pseudo-random numbers.
- Advantages:
- Simple to generate and globally unique.
- No information leakage (e.g., creation time).
- Disadvantages: Not time-sortable, which can lead to performance issues with database indexing on these IDs if they are primary keys in large, frequently inserted tables.
- Use Cases: General-purpose unique identifiers where time-ordering is not a requirement.
-
UUIDv5 (Name-Based, SHA-1 Hashed):
- How it works: Generates a deterministic UUID based on a "namespace" UUID and a "name" (a string). The same namespace and name will always produce the same UUIDv5.
- Advantages:
- Deterministic: Useful when you need to consistently derive the same ID for the same input, e.g., for content-addressable storage or identifying resources by a unique name.
- Globally unique within the combination of namespace and name.
- Disadvantages: Relies on SHA-1 (though for collision resistance in UUIDs, it's generally considered acceptable). Not time-sortable.
- Use Cases: Generating stable identifiers for files based on their content, deriving IDs for entities based on unique business keys, de-duplication.
MTI defaults to UUIDv7 for create_type_id() due to its excellent balance of global uniqueness and time-sortability, which covers a wide range of common application needs. You can explicitly choose other versions when specific characteristics are required.
Beyond V4, V5, and V7, which are commonly used and detailed above, the underlying typeid_suffix crate (and thus MTI through manual construction with TypeIdSuffix::new::<V>() and MagicTypeId::new()) also supports other standard UUID versions. These include Nil (for all-zero UUIDs), V1 (traditional time-based), V3 (name-based using MD5), and V6 (another time-based variant). While these versions are available for specific needs, V7 is generally recommended for new time-based requirements, and V5 for SHA-1 based named requirements, due to their modern advantages.
Quick Start
Add mti to your Cargo.toml:
[]
= "1.0" # Or the latest version
Optional Serde Support:
If you need to serialize or deserialize MagicTypeId instances (e.g., for use with Serde-compatible formats like JSON, YAML, etc.), enable the serde feature flag:
[]
= { = "1.0", = ["serde"] } # Or the latest version, ensure to match the version above
This will enable Serde's Serialize and Deserialize traits for MagicTypeId.
Optional Tracing Instrumentation:
For detailed operational insights, mti supports instrumentation via the tracing crate. When enabled, mti will emit trace events for key operations like ID creation and parsing. This is invaluable for debugging, performance analysis, and understanding the crate's behavior within your application.
To enable this feature, add instrument to the features list in your Cargo.toml:
[]
= { = "1.0", = ["instrument"] } # Or your current version
# Your application will also need a tracing subscriber
= "0.1" # The tracing facade
= { = "0.3", = ["fmt"] } # Example subscriber
How it Works:
When the instrument feature is active, mti functions are annotated with #[instrument(...)] and contain trace!, debug!, etc., calls from the tracing crate. Your application can then configure a tracing subscriber (like tracing-subscriber) to collect, filter, format, and output these trace events. This gives you control over the level of detail and destination of the trace data (e.g., console, file, or a distributed tracing system).
Example of setting up a basic subscriber in your application:
// In your application's main.rs or initialization code:
use FmtSpan;
use *; // For create_type_id and MagicTypeId
This setup allows the host application to effectively leverage the instrumentation within mti.
Then, in your Rust code:
use *;
// Create a MagicTypeId for a user (defaults to UUIDv7)
let user_id = "user".create_type_id;
println!; // e.g., "user_01h455vb4pex5vsknk084sn02q"
// Parse an existing MagicTypeId
let order_id_str = "order_01h455vb4pex5vsknk084sn02q";
match from_str
Core Features
-
Type-Safe Prefixes: Embed type information directly in your identifiers (e.g.,
user_,product_).- Benefit: Helps prevent accidental misuse of IDs, making your system more robust.
-
Human-Readable Identifiers: Prefixes make IDs self-descriptive and easy to understand at a glance.
- Benefit: Aids in debugging, log analysis, and database inspection.
-
UUID-Backed Uniqueness: Utilizes various UUID versions (V4, V5, V7) for global uniqueness. UUIDv7 is the default for new ID generation. (See "Understanding UUID Versions in MTI" above for details).
- Benefit: Helps eliminate ID collisions.
-
Time-Sortable (with UUIDv7): Identifiers generated with UUIDv7 are inherently sortable by time.
- Benefit: Simplifies ordering of events, records, and logs chronologically.
-
Flexible Prefix Handling: Supports both strict and sanitized prefix creation.
- Benefit: Adapt to various input requirements while maintaining valid TypeID formats.
-
Intuitive API: Ergonomic methods for creation, parsing, and manipulation.
- Benefit: Easy to integrate and use, reducing boilerplate code.
-
Zero-Cost Abstractions: Efficient string-like operations without performance overhead.
- Benefit: Good performance for critical path operations.
-
Optional Serde Support: Easily serialize and deserialize
MagicTypeIdinstances using Serde by enabling theserdefeature flag.- Benefit: Seamless integration with common serialization formats like JSON, YAML, TOML, etc., for data interchange and storage.
-
Optional Tracing Instrumentation: Enables detailed operational tracing using the
tracingcrate when theinstrumentfeature is active.- Benefit: Provides deep insights into the crate's internal workings for debugging and performance analysis, configurable by the host application's
tracingsubscriber.
- Benefit: Provides deep insights into the crate's internal workings for debugging and performance analysis, configurable by the host application's
Usage Examples
Creating MagicTypeIds with Different UUID Versions
use *;
use Uuid; // For UUIDv5 example
// Create with UUIDv7 (sortable, recommended default)
let product_id_v7 = "product".create_type_id; // V7 is default
// Or be explicit:
let explicit_product_id_v7 = "product".;
println!;
// Create with UUIDv4 (random)
let user_id_v4 = "user".;
println!;
// Create with UUIDv5 (name-based, deterministic)
let prefix_for_v5 = "resource";
let namespace_uuid = parse_str.unwrap; // Example namespace
let name_to_identify = "my_unique_resource_name";
// 1. Generate the UUIDv5
let generated_v5_uuid = new_v5;
// 2. Create the TypeIdPrefix (can be sanitized or strict)
// Using sanitized prefix creation for this example:
let type_id_prefix_v5 = create_sanitized;
// Alternatively, for a strict prefix (will error if prefix_for_v5 is invalid):
// let type_id_prefix_v5 = TypeIdPrefix::try_from(prefix_for_v5).expect("Prefix should be valid");
// 3. Create the TypeIdSuffix from the generated UUID
let type_id_suffix_v5 = from;
// 4. Combine them into a MagicTypeId
let resource_id_v5 = new;
println!;
// Create with a specific suffix (if you already have a TypeIdSuffix)
// Note: The generic V on create_type_id_with_suffix is not used for suffix generation
// when a suffix is already provided, but a valid UuidVersion type is still needed.
let existing_suffix = ; // Example: new V7 suffix
let order_id_custom = "order".;
println!;
Flexible Prefix Handling
use *;
// Sanitized creation (attempts to produce a valid prefix from potentially invalid input)
let sanitized_id = "Invalid Prefix!".create_type_id; // Defaults to V7
assert!;
assert_eq!;
// Strict creation (returns an error for invalid prefixes)
let result = "Invalid Prefix!".;
assert!;
println!;
String-Like Behavior
use *;
let id = "user".create_type_id; // Defaults to V7
assert!;
assert_eq!;
// Length can vary slightly based on prefix length, but suffix is fixed for a given UUID version's encoding.
// For a 4-char prefix and standard TypeID base32 encoding of a 128-bit UUID:
// Prefix (4) + Underscore (1) + Suffix (26 for V7/V4) = 31 characters
assert_eq!;
// Use in string comparisons
assert_eq!;
Parsing and Component Extraction
use *;
let id_str = "product_01h455vb4pex5vsknk084sn02q";
let magic_id = from_str.unwrap;
assert_eq!;
assert_eq!;
// Extract UUID
// You can get the underlying `uuid::Uuid` by accessing the suffix:
let uuid_val = magic_id.suffix.to_uuid;
println!;
// Alternatively, if the `MagicTypeIdExt` trait is in scope (e.g., via `use mti::prelude::*`),
// you can call `uuid()` or `uuid_str()` directly on the MagicTypeId instance.
// These methods return a Result, as parsing could fail if the TypeID string was malformed
// (though `magic_id` here is known to be valid).
let direct_uuid_res = magic_id.uuid; // Returns Result<Uuid, _>
if let Ok = direct_uuid_res
let direct_uuid_str_res = magic_id.uuid_str; // Returns Result<String, _>
if let Ok = direct_uuid_str_res
// You can check its version if needed
// println!("UUID Version: {:?}", uuid_val.get_version());
Sorting
When MagicTypeId is created with a V7 UUID, it provides a natural sorting order:
- Primary Sorting: By the timestamp in the
UUIDv7suffix. This means that identifiers generated later will appear after those generated earlier. - Secondary Sorting: If the timestamps are equal (unlikely with UUIDv7's millisecond precision and random bits), then sorting is based on the lexicographical order of the prefix. This ensures consistent ordering.
use FromStr;
use sleep;
use Duration;
use *;
use TypeIdPrefix; // Specific import for clarity
use TypeIdSuffix; // Specific import for clarity
let prefix1 = from_str.unwrap;
let prefix2 = from_str.unwrap;
// Generate with default V7
let id1 = new;
sleep; // Ensure different timestamps
let id2 = new;
// Create id3 with the same suffix as id2 but different prefix for testing secondary sort
let id3_suffix_val = id2.suffix.clone;
let id3 = new;
assert!;
// For secondary sort demonstration, ensure suffixes are identical if timestamps were hypothetically the same
// In practice, UUIDv7 makes identical timestamps + random bits extremely rare.
// Here, we explicitly made id3's suffix same as id2's for this test.
assert_eq!;
assert!;
Use Cases: Where MTI Shines
MagicTypeId is versatile and improves clarity and safety in various scenarios:
- Applications with Multiple Components or Services: Generate globally unique, type-aware identifiers that flow understandably across different parts of your application or between services.
Why it helps: Reduces ambiguity when correlating events or entities.
# use *; let order_id = "order".create_type_id; // Send to another component/service: e.g., "order_01h455vb4pex5vsknk084sn02q" // The receiving part immediately knows it's an order ID. println!; - Database Records: Create human-readable, sortable (with UUIDv7), and type-prefixed primary or secondary keys.
Why it helps: Makes database browsing and debugging easier.
user_01arZ...is more informative than a raw UUID.# use *; # # # ; # let db = DummyDb; # let user_data = "Alice"; let user_id = "user".create_type_id; // MagicTypeIds can be stored as strings in most databases db.insert_user; - API Development (REST/GraphQL): Use as clear, self-documenting resource identifiers in your API endpoints and payloads.
Why it helps: Improves API ergonomics for both producers and consumers.
# use *; # // Example, not a real feature here # use ; # # async // Example (conceptual, actual web framework integration may vary): // #[get("/users/{id}")] // async fn get_user(id: Path<MagicTypeId>) -> impl Responder { /* ... */ } println!; - Logging and Tracing: Embed type information directly in log entries for improved debugging and event correlation.
Why it helps: Quickly filter and understand logs related to specific entity types.
# use *; # let order_id = "order".create_type_id; process_order; // log::info!("Processing order {}", "order".create_type_id()); - Content-Addressable Storage (with UUIDv5): Generate deterministic, repeatable IDs based on content or unique names within a namespace. (See "Understanding UUID Versions in MTI" for more on UUIDv5).
Why it helps: Useful for de-duplication or creating stable identifiers for specific data.
# use *; // Brings in MagicTypeId, TypeIdPrefix, TypeIdSuffix, etc. # use Uuid; // For Uuid::new_v5 and Uuid::parse_str let prefix_str = "domain"; let namespace = parse_str.unwrap; // Example namespace let name = "example.com"; // 1. Generate the UUIDv5 let v5_uuid = new_v5; // 2. Create TypeIdPrefix let type_prefix = create_sanitized; // Or for a strict prefix: // let type_prefix = TypeIdPrefix::try_from(prefix_str).unwrap(); // 3. Create TypeIdSuffix from the UUID let type_suffix = from; // 4. Create MagicTypeId let domain_id = new; // Always produces the same ID for "example.com" within this namespace // The specific suffix depends on the namespace and name. println!;
Advanced Usage: Enhancing Type Safety with Newtypes
For stronger compile-time guarantees, wrap MagicTypeId in your own domain-specific ID types:
use *;
use FromStr;
// Add necessary derives
;
// Implement Display, Serialize, Deserialize etc. as needed, often by delegating to self.0
;
// Now, your functions can demand specific ID types:
let user_id = new;
let order_id = new;
process_user;
process_order;
// This would now cause a compile-time error, preventing accidental misuse!
// process_user(order_id);
Performance and Safety
mti is designed with performance and safety as priorities:
- Zero-Cost Abstractions: Many string-like operations on
MagicTypeIdare designed to be efficient. - Solid Foundation: Built upon the
TypeIdPrefixandTypeIdSuffixcrates. - Rust's Safety Guarantees: Leverages Rust's type system and ownership model to help prevent common programming errors at compile-time.
- Comprehensive Test Suite: Includes extensive unit and property-based tests to ensure correctness and reliability.
Acknowledgments
This crate implements version 0.3.0 of the TypeID Specification created and maintained by Jetify. Their work in developing and managing this specification is appreciated. The concept of prefixing UUIDs for better readability is also notably used by services like Stripe, which served as an inspiration for the broader adoption of such patterns.
Contributing
Contributions are welcome! If you encounter any issues, have feature requests, or want to contribute code, please visit the GitHub repository and open an issue or pull request.
License
This project is licensed under either of:
- Apache License, Version 2.0, (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.
About the Author
I'm @rrrodzilla, a technologist with industry experience, including roles as an SOA and cloud architect, and Principal Technical Product Manager at AWS for the Rust Programming Language. Currently, I'm the owner and operator of Govcraft, building and consulting on Rust and AI solutions.
For more information, visit https://www.govcraft.ai