Type-safe, K-sortable, globally unique identifiers inspired by Stripe IDs
⚠️ Early Stage Development This library is in active development (version 0.x). The API may change before reaching 1.0.0. While the library is production-ready and fully tested, you may encounter breaking changes in minor releases. See CHANGELOG.md for detailed change tracking.
TypeID is a modern, type-safe extension of UUIDv7. It adds an optional type prefix to UUIDs, making them more readable and self-documenting.
Example: user_01h5fskfsk4fpeqwnsyz5hj55t
user (optional, 0-63 lowercase characters)_ (omitted if no prefix)01h5fskfsk4fpeqwnsyz5hj55t (base32-encoded UUIDv7)Key Features:
{:deps {io.github.unisoma/typeid {:mvn/version "0.2.0"}}}
[io.github.unisoma/typeid "0.2.0"]
(require '[typeid.core :as typeid])
;; Create a new TypeID with a fresh UUIDv7
(typeid/create "user")
;;=> "user_01h5fskfsk4fpeqwnsyz5hj55t"
;; Create with keyword prefix (extracts name)
(typeid/create :user)
;;=> "user_01h5fskfsk4fpeqwnsyz5hj55t"
;; Create without prefix (nil or no args)
(typeid/create nil)
;;=> "01h5fskfsk4fpeqwnsyz5hj55t"
(typeid/create)
;;=> "01h5fskfsk4fpeqwnsyz5hj55t"
;; Create TypeID from an existing UUID
(typeid/create "user" #uuid "018c3f9e-9e4e-7a8a-8b2a-7e8e9e4e7a8a")
;;=> "user_01h455vb4pex5vsknk084sn02q"
;; Validate a TypeID (returns nil if valid, error map if invalid)
(typeid/explain "user_01h5fskfsk4fpeqwnsyz5hj55t")
;;=> nil ; Valid!
(typeid/explain "User_01h5fskfsk4fpeqwnsyz5hj55t")
;;=> {:type :typeid/invalid-prefix
;; :message "Invalid prefix: contains uppercase characters"
;; :input "User_01h5fskfsk4fpeqwnsyz5hj55t"
;; :expected "lowercase alphanumeric characters (a-z, 0-9)"
;; :actual "uppercase 'U'"}
;; Parse a TypeID into components (throws exception if invalid)
(typeid/parse "user_01h5fskfsk4fpeqwnsyz5hj55t")
;;=> {:prefix "user"
;; :suffix "01h5fskfsk4fpeqwnsyz5hj55t"
;; :uuid #uuid "018c3f9e-9e4e-7a8a-8b2a-7e8e9e4e7a8a"
;; :typeid "user_01h5fskfsk4fpeqwnsyz5hj55t"}
;; Low-level codec operations (typeid.codec namespace)
(require '[typeid.codec :as codec])
;; Encode UUID bytes to TypeID
(codec/encode uuid-bytes "user")
;;=> "user_01h5fskfsk4fpeqwnsyz5hj55t"
;; Decode TypeID to UUID bytes
(codec/decode "user_01h5fskfsk4fpeqwnsyz5hj55t")
;;=> #object["[B" 0x... (16-byte array)]
;; Convert UUID bytes to hex string
(codec/uuid->hex uuid-bytes)
;;=> "018c3f9e9e4e7a8a8b2a7e8e9e4e7a8a"
;; Convert hex string to UUID bytes
(codec/hex->uuid "018c3f9e-9e4e-7a8a-8b2a-7e8e9e4e7a8a")
;;=> #object["[B" ... (16-byte array)]
The library uses two patterns for error handling:
1. Validation without exceptions (explain)
Use explain when you want to check validity without catching exceptions:
(require '[typeid.core :as typeid])
;; Valid TypeID returns nil
(typeid/explain "user_01h5fskfsk4fpeqwnsyz5hj55t")
;;=> nil
;; Invalid TypeID returns error map
(typeid/explain "User_01h5fskfsk4fpeqwnsyz5hj55t")
;;=> {:type :typeid/invalid-prefix
;; :message "Invalid prefix: contains uppercase characters"
;; :input "User_01h5fskfsk4fpeqwnsyz5hj55t"
;; :expected "lowercase alphanumeric characters (a-z, 0-9)"
;; :actual "uppercase 'U'"}
;; Gracefully handles non-string input
(typeid/explain 12345)
;;=> {:type :typeid/invalid-input-type
;; :message "Invalid input type: expected string"
;; :input 12345
;; :expected "string"
;; :actual "number"}
2. Exception-based (parse, create)
Functions like parse and create throw ExceptionInfo on invalid input:
;; Parse valid TypeID
(typeid/parse "user_01h5fskfsk4fpeqwnsyz5hj55t")
;;=> {:prefix "user", :suffix "...", :uuid #uuid "...", :typeid "..."}
;; Invalid TypeID throws exception
(try
(typeid/parse "invalid-typeid")
(catch Exception e
(ex-data e)))
;;=> {:type :typeid/invalid-format
;; :message "Invalid format: does not match TypeID pattern"
;; :input "invalid-typeid"
;; :expected "TypeID format: [prefix_]suffix"}
;; Create with invalid prefix throws exception
(try
(typeid/create "InvalidPrefix" #uuid "018c3f9e-9e4e-7a8a-8b2a-7e8e9e4e7a8a")
(catch Exception e
(ex-data e)))
;;=> {:type :typeid/invalid-prefix
;; :message "Invalid prefix: contains uppercase characters"
;; :input "InvalidPrefix"
;; :expected "lowercase alphanumeric characters (a-z, 0-9)"
;; :actual "uppercase 'I'"}
Pattern: Validate before parsing (avoid exceptions)
(defn safe-parse [input]
(if-let [error (typeid/explain input)]
{:error error}
{:success (typeid/parse input)}))
(safe-parse "user_01h5fskfsk4fpeqwnsyz5hj55t")
;;=> {:success {:prefix "user", ...}}
(safe-parse "invalid")
;;=> {:error {:type :typeid/invalid-format, ...}}
The create function has three arities:
;; Zero-arity: Generate new TypeID with no prefix
(typeid/create)
;;=> "01h5fskfsk4fpeqwnsyz5hj55t"
;; One-arity: Generate new TypeID with prefix
(typeid/create "user")
;;=> "user_01h5fskfsk4fpeqwnsyz5hj55t"
(typeid/create :org) ; Keyword prefix works too
;;=> "org_01h5fskfsk4fpeqwnsyz5hj55t"
;; Two-arity: Create TypeID from existing UUID
(def my-uuid #uuid "018c3f9e-9e4e-7a8a-8b2a-7e8e9e4e7a8a")
(typeid/create "user" my-uuid)
;;=> "user_01h455vb4pex5vsknk084sn02q"
(typeid/create nil my-uuid) ; No prefix
;;=> "01h455vb4pex5vsknk084sn02q"
;; Works with any UUID version
(typeid/create "order" #uuid "550e8400-e29b-41d4-a716-446655440000")
;;=> "order_2qeh85amd9ct4vr9px628gkdkr"
For low-level operations, use the typeid.codec namespace:
(require '[typeid.codec :as codec])
;; Convert UUID bytes to hex string
(def uuid-bytes (byte-array [0x01 0x8c 0x3f 0x9e 0x9e 0x4e 0x7a 0x8a
0x8b 0x2a 0x7e 0x8e 0x9e 0x4e 0x7a 0x8a]))
(codec/uuid->hex uuid-bytes)
;;=> "018c3f9e9e4e7a8a8b2a7e8e9e4e7a8a"
;; Convert hex string to UUID bytes (accepts hyphens and uppercase)
(codec/hex->uuid "018c3f9e-9e4e-7a8a-8b2a-7e8e9e4e7a8a")
;;=> #object["[B" ... (16-byte array)]
(codec/hex->uuid "018C3F9E9E4E7A8A8B2A7E8E9E4E7A8A")
;;=> #object["[B" ... (same byte array)]
;; Encode UUID bytes directly
(codec/encode uuid-bytes "user")
;;=> "user_01h455vb4pex5vsknk084sn02q"
;; Decode TypeID to UUID bytes
(codec/decode "user_01h455vb4pex5vsknk084sn02q")
;;=> #object["[B" ... (16-byte array)]
Extract components from a TypeID:
;; Parse prefixed TypeID
(typeid/parse "user_01h5fskfsk4fpeqwnsyz5hj55t")
;;=> {:prefix "user"
;; :suffix "01h5fskfsk4fpeqwnsyz5hj55t"
;; :uuid #uuid "018c3f9e-9e4e-7a8a-8b2a-7e8e9e4e7a8a"
;; :typeid "user_01h5fskfsk4fpeqwnsyz5hj55t"}
;; Parse prefix-less TypeID
(typeid/parse "01h5fskfsk4fpeqwnsyz5hj55t")
;;=> {:prefix ""
;; :suffix "01h5fskfsk4fpeqwnsyz5hj55t"
;; :uuid #uuid "018c3f9e-9e4e-7a8a-8b2a-7e8e9e4e7a8a"
;; :typeid "01h5fskfsk4fpeqwnsyz5hj55t"}
;; Use destructuring to extract specific fields
(let [{:keys [prefix uuid]} (typeid/parse "user_01h5fskfsk4fpeqwnsyz5hj55t")]
(println "Type:" prefix)
(println "UUID:" uuid))
;;=> Type: user
;;=> UUID: 018c3f9e-9e4e-7a8a-8b2a-7e8e9e4e7a8a
(defn create-user [name email]
(let [user-id (typeid/create "user")]
{:id user-id
:name name
:email email
:created-at (System/currentTimeMillis)}))
(create-user "Alice" "alice@example.com")
;;=> {:id "user_01h5fskfsk4fpeqwnsyz5hj55t"
;; :name "Alice"
;; :email "alice@example.com"
;; :created-at 1699564800000}
(defn get-user-by-id [typeid-str]
(if-let [error (typeid/explain typeid-str)]
{:error (str "Invalid TypeID: " (:message error))}
(fetch-user-from-db typeid-str)))
(get-user-by-id "user_01h5fskfsk4fpeqwnsyz5hj55t")
;;=> {:id "user_..." :name "Alice" ...}
(get-user-by-id "invalid")
;;=> {:error "Invalid TypeID: Invalid format: does not match TypeID pattern"}
(defn save-and-return-order [order-data]
(let [order-id (typeid/create "order")
{:keys [uuid]} (typeid/parse order-id)]
;; Save UUID directly to database (platform-native type)
(save-to-db! {:uuid uuid :data order-data})
;; Return TypeID to client (human-readable)
{:id order-id :data order-data}))
(save-and-return-order {:items [...] :total 99.99})
;;=> {:id "order_01h5fskp7y2t3z9x8w6v5u4s3r" :data {...}}
(defn fetch-order [typeid-str]
(try
(let [{:keys [uuid]} (typeid/parse typeid-str)]
;; Use UUID directly in database queries
(query-db {:uuid uuid}))
(catch Exception e
{:error (str "Invalid TypeID: " (ex-message e))})))
(fetch-order "order_01h5fskp7y2t3z9x8w6v5u4s3r")
;;=> {:uuid #uuid "..." :items [...] :total 99.99}
(defn delete-user [typeid-str]
(try
(let [{:keys [prefix uuid]} (typeid/parse typeid-str)]
(if (= "user" prefix)
(do
(delete-from-db! uuid)
{:status :deleted :id typeid-str})
{:error "Expected user TypeID, got different type"}))
(catch Exception e
{:error (str "Invalid TypeID: " (ex-message e))})))
(delete-user "user_01h5fskfsk4fpeqwnsyz5hj55t")
;;=> {:status :deleted :id "user_01h5fskfsk4fpeqwnsyz5hj55t"}
(delete-user "order_01h5fskp7y2t3z9x8w6v5u4s3r")
;;=> {:error "Expected user TypeID, got different type"}
(require '[next.jdbc :as jdbc])
;; Store UUID as UUID column (works directly with java.util.UUID)
(defn insert-user [ds name email]
(let [user-id (typeid/create "user")
{:keys [uuid]} (typeid/parse user-id)]
(jdbc/execute-one! ds
["INSERT INTO users (id, name, email) VALUES (?, ?, ?)"
uuid name email])
{:typeid user-id :name name :email email}))
(defn find-user [ds typeid-str]
(let [{:keys [uuid]} (typeid/parse typeid-str)]
;; UUID object works directly with JDBC
(jdbc/execute-one! ds
["SELECT * FROM users WHERE id = ?" uuid])))
(require '[compojure.core :refer [defroutes GET POST]]
'[ring.util.response :refer [response status]])
(defroutes app-routes
(POST "/users" {body :body}
(let [user-id (typeid/create "user")]
(response {:id user-id
:name (:name body)
:email (:email body)})))
(GET "/users/:id" [id]
(if-let [error (typeid/explain id)]
(-> (response {:error (:message error)})
(status 400))
(response (fetch-user-from-db id)))))
The library is designed for high performance with the following target benchmarks:
| Operation | Target |
|---|---|
create | < 2μs |
parse | < 2μs |
encode/decode | < 1μs |
base32/encode | < 1μs |
base32/decode | < 1μs |
validate-prefix | < 500ns |
uuid/generate-uuidv7 | < 500ns |
These performance targets ensure TypeID operations remain negligible overhead in production systems:
Real-world throughput:
Compared to typical operations:
When performance matters:
Bottom line: At these speeds, TypeID operations are negligible compared to I/O costs (database, network, disk). You can generate IDs freely without worrying about performance bottlenecks.
Zero reflection warnings. All hot paths use type hints for optimal performance.
To measure actual performance on your system:
# Using Babashka (recommended)
bb bench
# Or manually with Clojure CLI
clojure -M:dev -m benchmarks.core-bench
The benchmark suite uses Criterium and reports:
| Feature | TypeID | UUID | ULID | Nano ID |
|---|---|---|---|---|
| Type prefix | ✅ | ❌ | ❌ | ❌ |
| K-sortable | ✅ | ❌ (v4), ⚠️ (v7) | ✅ | ❌ |
| Compact | ✅ (26 chars) | ❌ (36 chars) | ✅ (26 chars) | ✅ (21 chars) |
| URL-safe | ✅ | ⚠️ (with hyphens) | ✅ | ✅ |
| Spec compliance | ✅ v0.3.0 | ✅ RFC 4122 | ✅ | ❌ |
| Cross-platform | ✅ | ✅ | ✅ | ✅ |
"Prefix must be lowercase"
clojure.string/lower-case to normalize input if needed"First character of suffix must be 0-7"
8-z to prevent 128-bit overflow"Suffix must be exactly 26 characters"
"Prefix too long (max 63 characters)"
ClojureScript: UUID generation uses js/Date.now() and crypto.getRandomValues(), which require:
ws dependency (see package.json)JVM: Uses System.currentTimeMillis() and java.security.SecureRandom
Can I use TypeIDs as database primary keys?
Yes! Store the UUID portion (16 bytes or hex string) as the primary key, and generate the TypeID string when sending data to clients. This gives you the best of both worlds: efficient storage and human-readable IDs in your API.
;; Store UUID in database
(let [user-id (typeid/create "user")
{:keys [uuid]} (typeid/parse user-id)]
(save-to-db! {:id uuid :name "Alice"}))
;; Return TypeID to clients
(defn get-user [uuid]
(let [user (fetch-from-db uuid)
user-id (typeid/create "user" (:id user))]
(assoc user :id user-id)))
Are TypeIDs sortable?
Yes! TypeIDs are chronologically sortable because they're based on UUIDv7, which encodes the timestamp in the first 48 bits. This means TypeIDs created later will sort after TypeIDs created earlier.
(def id1 (typeid/create "user"))
(Thread/sleep 10)
(def id2 (typeid/create "user"))
(< id1 id2) ;;=> true (id1 was created first)
Can I have TypeIDs without a prefix?
Yes! Use nil or no arguments to create prefix-less TypeIDs:
(typeid/create nil)
;;=> "01h5fskfsk4fpeqwnsyz5hj55t"
(typeid/create)
;;=> "01h5fskfsk4fpeqwnsyz5hj55t"
Are single-character prefixes allowed?
Technically yes, but the spec recommends at least 3 characters for clarity. Use descriptive prefixes like "user" instead of "u" to make your IDs more self-documenting.
Can I encode UUIDv4 (or other non-UUIDv7) as a TypeID?
Yes! The create function with two arguments accepts any UUID:
(typeid/create "legacy" #uuid "550e8400-e29b-41d4-a716-446655440000")
;;=> "legacy_2qeh85amd9ct4vr9px628gkdkr"
Note: Only the one-argument form generates UUIDv7. Other UUID versions won't be chronologically sortable.
How do I migrate from UUIDs to TypeIDs?
If you have existing UUIDs in your system, encode them with a prefix:
(require '[typeid.codec :as codec])
;; From UUID object
(typeid/create "user" existing-uuid)
;; From hex string
(let [uuid-bytes (codec/hex->uuid "018c3f9e-9e4e-7a8a-8b2a-7e8e9e4e7a8a")]
(codec/encode uuid-bytes "user"))
The underlying UUID remains the same, so your database queries continue to work.
What's the performance impact?
TypeID operations are extremely fast (< 2μs for create/parse). At 2μs per operation, you can generate 500K TypeIDs per second on a single core. This is negligible compared to typical I/O operations:
Even at high traffic (10K req/s), ID generation consumes less than 2% CPU time.
Do I need to validate TypeIDs from my own system?
If you're generating TypeIDs within your system and storing them, you generally don't need to validate them again when reading from your database. However, always validate TypeIDs that come from:
;; From database (trusted) - no validation needed
(defn get-user-from-db [uuid]
(let [user (fetch-user uuid)]
(typeid/create "user" (:id user))))
;; From user input (untrusted) - validate first
(defn get-user-by-id [typeid-str]
(if-let [error (typeid/explain typeid-str)]
{:error (:message error)}
(fetch-user typeid-str)))
Can I customize the prefix separator?
No, the TypeID specification requires the underscore (_) separator. This ensures consistency across all TypeID implementations and languages.
How do I get UUID bytes if I need them?
In version 0.2.0+, parse returns platform-native UUID objects (not byte arrays). If you need the raw bytes:
(require '[typeid.impl.uuid :as uuid])
(let [{:keys [uuid]} (typeid/parse "user_01h5fskfsk4fpeqwnsyz5hj55t")
uuid-bytes (uuid/uuid->bytes uuid)]
;; uuid-bytes is a 16-byte array
(alength uuid-bytes))
;;=> 16
For most use cases, you don't need the bytes - UUID objects work directly with:
str)=)What changed in v0.2.0?
Version 0.2.0 introduced a breaking change: parse now returns platform-native UUID objects instead of byte arrays.
;; v0.1.x (old)
(:uuid (typeid/parse "user_..."))
;;=> #object["[B" ... (byte array)]
;; v0.2.0+ (new)
(:uuid (typeid/parse "user_..."))
;;=> #uuid "018c3f9e-9e4e-7a8a-8b2a-7e8e9e4e7a8a"
Benefits:
=Migration: If your code expects byte arrays, use typeid.impl.uuid/uuid->bytes to convert. See the quickstart guide for detailed migration steps.
This project includes Babashka tasks for common development workflows:
# Show all available tasks
bb tasks
# Show project information
bb info
# Testing
bb test # Run all JVM tests
bb test:watch # Run tests in watch mode
bb test:cljs # Run ClojureScript tests
bb test:coverage # Run with coverage report
# Code quality
bb lint # Run clj-kondo
bb format # Format code with cljfmt
bb quality # Run all quality checks
# Performance
bb bench # Run benchmarks
# Build
bb build # Clean and build JAR
bb install # Install to local Maven repo
# CI workflows
bb ci:check # Run all CI checks
bb release:check # Verify release readiness
# Deploy
bb deploy # Deploy to Clojars
# JVM tests
clojure -M:test -m kaocha.runner
# ClojureScript tests (requires Node.js)
npm install
clojure -M:test:cljs -m kaocha.runner --config-file tests.cljs.edn
# Test coverage
clojure -M:test:coverage
# Linting and formatting
clojure -M:lint
clojure -M:dev -m cljfmt.main fix src test
# Benchmarks
clojure -M:dev -m benchmarks.core-bench
# REPL
clojure -M:nrepl
See CONTRIBUTING.md for detailed development setup instructions.
Contributions are welcome! Please see CONTRIBUTING.md for:
Copyright © 2025 Jonas Rodrigues
Distributed under the MIT License. See LICENSE file for details.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |