A pure, tagless-final ULID implementation for Scala, built on Cats and Cats Effect.
- Pure Scala implementation — no external ULID dependencies
- Cross-platform — JVM, Scala.js, and Scala Native
- Cross-build — Scala 2.13 and Scala 3
- Tagless-final — polymorphic in effect type
F[_] - Type-safe —
Ulidvalue type (opaque in Scala 3, AnyVal in Scala 2) - Monotonic generation — optional strictly-increasing ULIDs within the same millisecond
- Testable — injectable
RandomSourceandTimestampProviderfor deterministic tests
Add the dependency to your build.sbt:
libraryDependencies += "de.thatscalaguy" %%% "ulid4cats" % "2.0.0"For JVM-only projects, you can use %% instead of %%%.
import cats.effect.{IO, IOApp, ExitCode}
import de.thatscalaguy.ulid4cats.{FULID, Ulid}
object Main extends IOApp:
def run(args: List[String]): IO[ExitCode] = for {
// Generate a typed Ulid
ulid <- FULID[IO].generateUlid
_ <- IO.println(s"ULID: ${ulid.value}")
_ <- IO.println(s"Timestamp: ${ulid.timestamp}")
// Or generate as String (backward compatible)
ulidStr <- FULID[IO].generate
_ <- IO.println(s"ULID String: $ulidStr")
} yield ExitCode.Successimport cats.effect.{IO, IOApp, ExitCode}
import de.thatscalaguy.ulid4cats.{FULID, Ulid}
object Main extends IOApp {
def run(args: List[String]): IO[ExitCode] = for {
ulid <- FULID[IO].generateUlid
_ <- IO.println(s"ULID: ${ulid.value}")
} yield ExitCode.Success
}For more control, use the UlidGen algebra directly:
import cats.effect.IO
import de.thatscalaguy.ulid4cats.UlidGen
// Random generator (new randomness each call)
val randomGen: UlidGen[IO] = UlidGen.randomDefault[IO]
// Monotonic generator (strictly increasing within same millisecond)
val monotonicGen: IO[UlidGen[IO]] = UlidGen.monotonicDefault[IO]import de.thatscalaguy.ulid4cats.{Ulid, UlidCodec}
// Safe parsing
val parsed: Either[UlidError, Ulid] = Ulid.fromString("01ARZ3NDEKTSV4RRFFQ69G5FAV")
val parsedOpt: Option[Ulid] = Ulid.fromStringOption("01ARZ3NDEKTSV4RRFFQ69G5FAV")
// Unsafe parsing (throws on invalid input)
val ulid: Ulid = Ulid.unsafeFromString("01ARZ3NDEKTSV4RRFFQ69G5FAV")
// Validation
val isValid: Boolean = UlidCodec.isValid("01ARZ3NDEKTSV4RRFFQ69G5FAV")
// Extract components
val timestamp: Long = ulid.timestamp
val bytes: Array[Byte] = ulid.toBytesInject test doubles for reproducible tests:
import cats.effect.IO
import de.thatscalaguy.ulid4cats.{UlidGen, RandomSource, TimestampProvider}
val fixedTimestamp = 1702300800000L
val fixedRandomness = Array.fill[Byte](10)(0x42)
implicit val randomSource: RandomSource[IO] = RandomSource.constant[IO](fixedRandomness)
implicit val timestampProvider: TimestampProvider[IO] = TimestampProvider.constant[IO](fixedTimestamp)
val deterministicGen: UlidGen[IO] = UlidGen.random[IO]
// All generated ULIDs will have the same timestamp and randomnessUlid.fromString(s: String): Either[UlidError, Ulid]Ulid.fromStringOption(s: String): Option[Ulid]Ulid.unsafeFromString(s: String): UlidUlid.fromBytes(bytes: Array[Byte]): Either[UlidError, Ulid]ulid.value: String— the 26-character ULID stringulid.timestamp: Long— milliseconds since Unix epochulid.toBytes: Array[Byte]— 16-byte representation
UlidGen.random[F]— random generator (requires implicitRandomSourceandTimestampProvider)UlidGen.randomDefault[F]— random generator with default implsUlidGen.monotonic[F]— monotonic generator (strictly increasing)UlidGen.monotonicDefault[F]— monotonic generator with default impls
FULID[F].generate: F[String]— generate ULID as StringFULID[F].generateUlid: F[Ulid]— generate typed UlidFULID[F].isValid(s: String): F[Boolean]FULID[F].timeStamp(s: String): F[Option[Long]]FULID[F].parseUlid(s: String): F[Option[Ulid]]
This library implements the ULID specification:
- 128-bit identifier (same size as UUID)
- 26-character Crockford Base32 encoding
- Lexicographically sortable
- Case-insensitive (normalized to uppercase)
- 48-bit timestamp (milliseconds since Unix epoch)
- 80-bit randomness
MIT License — see LICENSE for details.
