A lightweight, zero-dependency Clojure toolkit for generating and parsing unique identifiers (ULID, UUID v4, UUIDv7, and more).
Identika provides a collection of modern unique identifier strategies, each in its own self-contained namespace. Designed for database primary keys, distributed tracing, log collation, and client-safe obfuscation — using idiomatic Clojure without pulling in heavy transitive dependencies.
org.clojure/clojure) and standard JDK classes (java.security.SecureRandom, etc.).SecureRandom and are shared across calls.| Format | Sortable? | Length / Representation | Key Advantages & Best Use Case | Status |
|---|---|---|---|---|
| UUID v4 | No | 36 chars (hex-hyphens) / 16 bytes | RFC 4122 random UUID. Universal standard, widely supported. | ✅ Complete |
| ULID | Yes | 26 chars (Crockford Base32) / 16 bytes | Millisecond-precision sorting, URL-safe, case-insensitive. Excellent for DB keys. | ✅ Complete |
| UUIDv7 | Yes | 36 chars (Hex-Hyphens) / 16 bytes | RFC 9562 time-ordered UUID. Seamless drop-in for traditional UUIDs. | ✅ Complete |
| KSUID | Yes | 27 chars (Base62) / 20 bytes | 32-bit second-precision timestamp + 128-bit random payload. | ⏳ Planned |
| NanoID | No | Customizable (default 21 chars) | Compact, highly secure, custom alphabets. Great for user-facing short IDs. | ⏳ Planned |
| CUID2 | No | Customizable (default 24 chars) | Secure, collision-resistant, horizontally-scalable IDs. | ⏳ Planned |
| HashID | No | Variable based on input integer | Reversible obfuscation for auto-incrementing IDs. | ⏳ Planned |
| FlakeID | Yes | 64-bit Long / Hex | Snowflake-style distributed ID (64-bit integer space). | ⏳ Planned |
Add Identika to your deps.edn dependencies:
org.clojars.rodriguesgot/identika {:mvn/version "0.2.0"}
(require '[identika.uuid :as uuid]
'[identika.uuid7 :as uuid7]
'[identika.ulid :as ulid])
;; Generate a UUID v4
(uuid/gen)
;; => "550e8400-e29b-41d4-a716-446655440000"
;; Generate a time-ordered UUIDv7
(uuid7/gen)
;; => "0190f0e2-3b9a-7c4d-9e5f-8a1b2c3d4e5f"
;; Generate a ULID
(ulid/gen)
;; => "01ARZ3NDEKTSV4RRFFQ69G5FAV"
Generates 36-character hex-hyphenated strings with proper version (0100) and variant (10xx) bits.
(require '[identika.uuid :as uuid])
;; Generate
(uuid/gen)
;; => "550e8400-e29b-41d4-a716-446655440000"
;; Validate
(uuid/valid? "550e8400-e29b-41d4-a716-446655440000")
;; => true
(uuid/valid? "not-a-uuid")
;; => false
;; Decode a UUID string into a 16-byte array
(uuid/decode "550e8400-e29b-41d4-a716-446655440000")
;; => #object["[B" ...]
;; Encode a 16-byte array back into a UUID string
(uuid/encode (byte-array 16 (range 16)))
;; => "00010203-0405-0607-0809-0a0b0c0d0e0f"
UUID v4 is not time-sortable and does not support monotonic operations. The namespace only includes gen, valid?, decode, and encode.
Time-ordered UUIDs with the same 36-character hex-hyphenated shape as UUID v4, so they drop straight into existing UUID columns, indexes, and clients. 128 bits laid out as:
rand_a, bytes 6-7), with version 7 (0111) in byte 6rand_b, bytes 8-15), with variant 10 in byte 8Because the timestamp occupies the most significant bits, UUIDv7 values sort chronologically as plain strings.
(require '[identika.uuid7 :as uuid7])
;; Generate using current system time
(uuid7/gen)
;; => "019ebd32-2666-7168-b09b-2240203908ea"
;; Generate with a specific timestamp (millisecond epoch)
(uuid7/gen 1781290640000)
;; => "019ebd32-2280-7168-b09b-2240203908ea"
;; Timestamps must fit in 48 bits, otherwise gen throws
(uuid7/gen 281474976710656)
;; => throws IllegalArgumentException
(uuid7/valid? "019ebd32-2280-7168-b09b-2240203908ea")
;; => true
;; UUID v4 and ULID strings are rejected — each namespace validates its own format
(uuid7/valid? "550e8400-e29b-41d4-a716-446655440000")
;; => false
;; Extract the millisecond timestamp from the leading 48 bits
(uuid7/timestamp "019ebd32-2280-7168-b09b-2240203908ea")
;; => 1781290640000
;; The canonical example from RFC 9562 §5.7
(uuid7/timestamp "017f22e2-79b0-7cc3-98c4-dc0c0c07398f")
;; => 1645557742000
;; Returns nil for anything that is not a UUIDv7
(uuid7/timestamp "not-a-uuid")
;; => nil
;; Decode a UUIDv7 string into a 16-byte array
(uuid7/decode "019ebd32-2280-7168-b09b-2240203908ea")
;; => #object["[B" ...]
;; Encode a 16-byte array back into a UUIDv7 string
(uuid7/encode (byte-array 16 (range 16)))
;; => "00010203-0405-0607-0809-0a0b0c0d0e0f"
Plain gen is time-ordered but not monotonic — two UUIDs generated in the same millisecond can sort in either order, because their random payloads are unrelated. monotonic closes that gap by incrementing the 74-bit random payload instead of re-rolling it, which is the bit-increment counter method from RFC 9562 §6.2.1:
;; The state atom holds the last UUIDv7 handed out
(def state (atom nil))
(uuid7/monotonic state)
;; => "01a101c5-59d2-7f20-b733-56ea3b3d2bb6"
;; Same millisecond: the payload is incremented, so ordering is guaranteed
(uuid7/monotonic state)
;; => "01a101c5-59d2-7f20-b733-56ea3b3d2bb7"
Details worth knowing:
rand_b first, carries into rand_a, and never disturbs the version nibble or variant bits — every value returned is still a valid UUIDv7.Use monotonic (via a dedicated atom per generator) when you need a strict insertion order — index locality in B-tree primary keys, for instance. Use plain gen when you want independence between calls and are happy with millisecond-level ordering only.
ULIDs are 128-bit identifiers consisting of:
SecureRandom)(require '[identika.ulid :as ulid])
;; Generate using current system time
(ulid/gen)
;; => "01ARZ3NDEKTSV4RRFFQ69G5FAV"
;; Generate with a specific timestamp (millisecond epoch)
(ulid/gen 1781290640998)
;; => "01ARZ3NDEKTSV4RRFFQ69G5FAV"
;; Validate a ULID string
(ulid/valid? "01ARZ3NDEKTSV4RRFFQ69G5FAV")
;; => true
(ulid/valid? "invalid-ulid!")
;; => false
;; Extract the millisecond timestamp
(ulid/timestamp "01ARZ3NDEKTSV4RRFFQ69G5FAV")
;; => 1781290640998
;; Returns nil for invalid ULIDs
(ulid/timestamp "not-a-ulid")
;; => nil
decode and encode are inverses — encode ∘ decode = id:
;; Decode a ULID string into a 16-byte array
(ulid/decode "01ARZ3NDEKTSV4RRFFQ69G5FAV")
;; => #object["[B" ...]
;; Encode a 16-byte array back into a ULID string
(ulid/encode (ulid/decode "01ARZ3NDEKTSV4RRFFQ69G5FAV"))
;; => "01ARZ3NDEKTSV4RRFFQ69G5FAV"
;; Get the next lexicographical ULID (increments random component)
(ulid/next-ulid "01ARZ3NDEKTSV4RRFFQ69G5FAV")
;; => "01ARZ3NDEKTSV4RRFFQ69G5FAW"
;; Monotonic generation via state atom
(def ulid-state (atom nil))
(ulid/monotonic ulid-state)
;; => "01ARZ3NDEKTSV4RRFFQ69G5FAV"
;; Next call within same millisecond increments instead of re-rolling entropy
(ulid/monotonic ulid-state)
;; => "01ARZ3NDEKTSV4RRFFQ69G5FAW"
identika.ulid| Function | Description |
|---|---|
(gen) / (gen timestamp) | Generate a ULID string |
(valid? s) | Returns true if s is a valid 26-char Crockford Base32 ULID |
(timestamp s) | Extract millisecond timestamp, or nil |
(decode s) | Decode ULID string → 16-byte array; nil if invalid |
(encode byte-arr) | Encode 16-byte array → ULID string; throws if not 16 bytes |
(next-ulid s) | Next lexicographic ULID; nil if invalid |
(monotonic state-atom) | Monotonically increasing ULIDs via state atom |
identika.uuid| Function | Description |
|---|---|
(gen) | Generate a UUID v4 string |
(valid? s) | Returns true if s is a valid RFC 4122 UUID v4 |
(decode s) | Decode UUID string → 16-byte array; nil if invalid |
(encode byte-arr) | Encode 16-byte array → UUID string; throws if not 16 bytes |
identika.uuid7| Function | Description |
|---|---|
(gen) / (gen millis) | Generate a UUIDv7 string at the current time, or at an explicit millisecond epoch; throws if millis does not fit in 48 bits |
(valid? s) | Returns true if s is a valid RFC 9562 UUIDv7 (version 7, variant 10xx); accepts uppercase hex |
(timestamp s) | Extract the millisecond Unix timestamp from the leading 48 bits, or nil if invalid |
(decode s) | Decode UUIDv7 string → 16-byte array; nil if invalid |
(encode byte-arr) | Encode 16-byte array → UUIDv7 string; throws if not 16 bytes |
(monotonic state-atom) | Monotonically increasing UUIDv7s via state atom, incrementing the random payload within a millisecond |
clojure -T:build jar
Produces target/identika-<version>.jar. Version defaults to 0.2.0 and can be overridden with the PROJECT_VERSION environment variable:
PROJECT_VERSION=1.0.0 clojure -T:build jar
clojure -T:build install
This installs the jar to your local ~/.m2/repository so other projects on your machine can depend on it.
Deploys use slipset/deps-deploy directly from the build — no Maven required.
CLOJARS_USERNAME=<clojars-username> \
CLOJARS_PASSWORD=<deploy-token> \
clojure -T:build deploy
This builds the jar and publishes it to https://clojars.org/repo under org.clojars.rodriguesgot/identika.
Note: the
org.clojars.<username>group is verified automatically for your account, so no additional group setup is needed.
Pushing a tag that starts with v runs the release workflow, which lints, tests, and deploys to Clojars using the version encoded in the tag:
git tag v0.2.0
git push origin v0.2.0
The workflow requires these repository secrets (Settings → Secrets and variables → Actions):
| Secret | Value |
|---|---|
CLOJARS_USERNAME | Your Clojars username |
CLOJARS_PASSWORD | A Clojars deploy token |
Requires GPG signing and a Sonatype account. After building:
mvn deploy:deploy-file \
-Dfile=target/identika-<version>.jar \
-DpomFile=target/classes/META-INF/maven/org.clojars.rodriguesgot/identika/pom.xml \
-DrepositoryId=sonatype \
-Durl=https://oss.sonatype.org/service/local/staging/deploy/maven2/ \
-Dgpg.sign=true
clojure -T:build clean
Identika uses Kaocha for testing:
clojure -M:test/unit
clojure -M:repl
next-ulid, monotonic generationDistributed under the MIT License.
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 |