Skip to content

Kairo IDs

Kairo IDs are safe, meaningful, and efficient. kairo-id is an alternative to raw UUIDs or serial IDs, improving developer experience and operational clarity.

  • Semantic prefixes: IDs tell you what they represent (user_123, business_123).
  • Strong entropy: As much or more randomness than UUIDs, tunable by payload length.
  • Compile-time safety: No more accidentally swapping IDs of different entity types.
  • Zero runtime overhead: Powered by Kotlin value classes (inlined to strings).

An example Kairo ID is user_ccU4Rn4DKVjCMqt3d0oAw3.

  • Prefix: user (human-readable entity type).
  • Payload: ccU4Rn4DKVjCMqt3d0oAw3 (base-62 encoded randomness).

The entropy of Kairo IDs depends on the length of the payload portion.

ID typeEntropy
Kairo ID, length 529.8 bits (equivalent)
Integer (positive only)32 bits
Kairo ID, length 847.6 bits (equivalent)
Long (positive only)64 bits
Kairo ID, length 1589.3 bits (equivalent)
UUID (version 4)122 bits
Kairo ID, length 22131.0 bits (equivalent)
Kairo ID, length 32190.5 bits (equivalent)

Entropy calculation: length * log2(62).

  • 22: Slightly higher entropy than UUIDs.
  • 15: Good balance of entropy and readability (default).
  • 5-8: Only if a small keyspace is acceptable.

Note: like any ID scheme, Kairo IDs involve tradeoffs — generation cost, DB storage size, and index performance. Use Kairo IDs when human clarity and safety outweigh those tradeoffs.

Install kairo-id.

build.gradle.kts
dependencies {
implementation("com.highbeam.kairo:kairo-id")
}

Each entity should define its own ID type — you can’t use Id directly.

@JvmInline
value class UserId(override val value: String) : Id {
init {
require(regex.matches(value)) { "Malformed user ID (value=$value)." }
}
companion object : Id.Companion<UserId>() {
val regex: Regex = regex(prefix = Regex("user"))
override fun create(payload: String): UserId =
UserId("user_$payload")
}
}

This enforces compile-time safety: IDs can’t be mixed up. Without semantic IDs, callers could accidentally swap userId and businessId. With Kairo IDs, the compiler prevents this mistake.

fun listRoles(businessId: BusinessId, userId: UserId): List<Role> {
// ...
}

Now go ahead and generate some user IDs!

UserId.random()
// => UserId("user_ccU4Rn4DKVjCMqt3d0oAw3")

The default payload length is 15 characters (~89 bits of entropy). Override it in your companion to use a different length.

companion object : Id.Companion<UserId>(length = 22) {
// Now generates 22-character payloads (~131 bits of entropy).
// ...
}

Id.Companion.regex() builds a regex that matches the prefix, an underscore, and the correct payload length for your ID type. Use it in the init block to validate incoming IDs.

val regex: Regex = regex(prefix = Regex("user"))
// Matches "user_" followed by exactly 15 base-62 characters.