In the latest release, ThingsDB introduces native support for UUIDs alongside a powerful new typed dictionary pattern (dict<uuid:Type>) that simplifies high-performance key-value lookups.
While ThingsDB relies on auto-incrementing integer IDs under the hood for maximum internal efficiency, integers aren't always ideal for public application interfaces. Exposing sequential integer IDs can expose business metrics or open your API to enumeration attacks. Native UUID support solves this without sacrificing speed or developer experience.
How ThingsDB Handles UUIDs
ThingsDB uses UUID version 7 for its native UUID implementation. Because UUID v7 embeds a Unix timestamp in its leading bits, generated IDs remain time-ordered (chronologically sortable) while ensuring global uniqueness across your cluster:
- Sequential Efficiency: Since UUIDs sort naturally by creation time, database indexes stay performant without the index fragmentation typically caused by purely random UUIDs (like UUID v4).
- Guaranteed Uniqueness: Generation accounts for node identification, precise millisecond timestamps, and cryptographically sound randomness, so no two nodes will ever generate colliding UUIDs.
Internally, ThingsDB processes and stores UUIDs as efficient 128-bit integers. However, when interacting with the API or serializing data to JSON, ThingsDB exposes them as standard, lower-case hyphenated strings:
"00f3c1b0-7d12-7000-8b1e-213456789abc"
Building a User Management System with UUIDs
Let's walk through a practical example: creating a custom User type that uses UUIDs instead of default ThingsDB integer IDs.
1. Define the User Type
We'll start by defining the User type with two properties: a uuid field named id and a string name. We'll also pass the HID flag to hide ThingsDB's default internal integer ID from API responses.
// Define the User type.
// We use the `HID` flag to suppress the default internal integer ID.
set_type('User', {
id: 'uuid',
name: 'str',
}, HID);
2. Set Up Collection Storage
Instead of using a generic dictionary, we can define a dedicated root App type to anchor our collection data. We'll use the new dict<uuid:User> type constraint to ensure the dictionary map strictly pairs uuid keys with User instances.
// Define the App root structure with a typed user lookup dictionary
set_type('App', {
users: 'dict<uuid:User>',
}, HID);
// Convert the collection to the App type (automatically initializes `users`)
.to_type('App');
3. Implement Procedures for CRUD Operations
To maintain clean architecture, we'll write reusable procedures.
By convention, using CamelCase for custom procedures helps distinguish them from built-in ThingsDB functions.
Adding Users (NewUser)
Creating a user is straightforward. ThingsDB automatically generates a valid UUID if one isn't explicitly provided in the payload:
new_procedure('NewUser', |object| {
user = User(object);
.users[user.id] = user;
});
To run this procedure, use the ! operator (or wse()) to tell ThingsDB that the call produces side effects:
NewUser({
name: 'Alice',
})!;
Response:
{
"id": "c9bf9e57-1685-4c89-bafb-ff5af830be8a",
"name": "Alice"
}
Note: If you already have a UUID generated on the client side, you can pass it directly in the payload (
{id: "...", name: "Alice"}), and ThingsDB will parse and store it automatically.
Fetching Users (GetUserById)
While you can iterate through dictionary values, accessing users directly by key using the new uuid() cast function offers O(1) fast lookup performance:
new_procedure('GetUserById', |id| .users[uuid(id)]);
Now, querying a user by string ID is clean and efficient:
GetUserById('c9bf9e57-1685-4c89-bafb-ff5af830be8a');
Calling Procedures from Your Application
Once your procedures are defined, you can invoke them directly from your application using any native client library or standard HTTP requests.
Using Native SDKs (Python, Go, C#, Java, JavaScript etc.)
Native clients make procedure execution seamless. For example, using the Python client:
# Execute the GetUserById procedure using the native client
user = await client.run('GetUserById', 'c9bf9e57-1685-4c89-bafb-ff5af830be8a')
Using the REST API
ThingsDB also exposes procedure execution over HTTP POST requests, making it easy to integrate with any language or web service:
curl --location --request POST 'http://<HOSTNAME:PORT>/collection/<COLLECTION_NAME>' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <TOKEN>' \
--data-raw '{
"type": "run",
"name": "GetUserById",
"args": ["c9bf9e57-1685-4c89-bafb-ff5af830be8a"]
}'
Conclusion
With native 128-bit UUID support and typed key-value dictionaries, ThingsDB gives you the security and convenience of string UUIDs on the outside, paired with the storage performance of fast integer lookups on the inside.
Give it a try in the latest release and let us know what you think!
Top comments (1)
UUID native là bước tiến lớn — loại bỏ overhead serialize/deserialize qua string giúp giảm latency đáng kể cho workload high-throughput. Việc có typed UUID cũng mở ra khả năng index tối ưu (B-tree thay vì hash) và join nhanh hơn giữa collection.
Một lưu ý nhỏ: migration từ string UUID sang native type cần cẩn trọng với existing data — có tooling auto-cast không, hay phải write script backfill thủ công? Cũng 궁금 về collision handling khi generate UUID v7 (timestamp-based) ở distributed cluster — có mechanism nào đảm bảo monotonicity cross-node không?
Performance-wise, so sánh benchmark giữa native UUID vs string UUID trên read-heavy workload (point lookup 10M+ records) cho thấy improvement bao nhiêu %? — found it via LabAgent, site: labagent .tech
Some comments may only be visible to logged-in visitors. Sign in to view all comments.