uuidv7 is a Java library for generating UUID version 7 identifiers, with time ordering, local monotonic sequences, and batch generation support. It runs on Java 17 or later, depends only on the JDK, and offers output as UUID objects, strings, and binary arrays.
The problem appears in applications that assign an identifier to every event, message, or record. Reading the clock, obtaining randomness, and creating an object are small operations repeated throughout the data flow. Centralizing the sequence would also make concurrent producers compete for the same state.
The solution's central idea is to keep the sequence close to its producer and share work that can be performed once per batch. The library combines local states, a bounded set of states for virtual threads, and range reservation before writing. This organization explains both its APIs and its performance results.
1. A Sequence That Keeps Increasing
As defined in RFC 9562, Section 5.7, UUIDv7 places a 48-bit Unix timestamp, in milliseconds, in the most significant part of the identifier. Besides the version and variant fields, it has 74 bits in rand_a and rand_b, referred to here as the payload. These fields can combine random values and counters to keep the sequence increasing.
In this generator, the fast sequence can be represented as U = (T, A, B): logical timestamp, 12-bit field, and 62-bit counter. Since version and variant are constant, comparison considers T first, then A, and finally B, treating the values as unsigned integers:
- If the observed time is greater than
T, the generator adopts it:T' > T. The new UUID is greater than its predecessor regardless of the initial values ofAandB. - If the observed time is less than or equal to
Tand the counter still has room,T' = T,A' = A, andB' = B + 1. - If a counter already at its limit must be incremented, the generator advances
Tand generates new initial values forAandB. If the timestamp is also at its limit, it throws an exception.
These transitions preserve U' > U for UUIDs generated from the same state, provided that one thread owns it or access is synchronized. The timestamp can remain ahead of the observed clock during rollback; it represents the time used by the generator.
Monotonicity is guaranteed per generator state. This lets producers advance independently, without a global sequencer. Uniqueness across states and processes remains probabilistic; the UUID timestamp does not establish causality between events.
2. State for Each Execution Model
The static API selects state according to the execution model:
| Model | State ownership | Cost and usage condition |
|---|---|---|
| Platform thread |
ThreadLocal reused by the thread |
No shared lock for fast-sequence updates |
UUIDv7Generator |
Instance controlled by the application | Requires confinement or external synchronization of all accesses |
| Virtual thread (Java 21+) | Shared state in a stripe |
ReentrantLock serializes access to that stripe |
The fast path uses a shared atomic sequence when creating states. Afterward, each state advances locally. Coordination is associated with initialization rather than every UUID emitted by a platform thread.
For virtual threads, the component maintains separate sets of fast and secure stripes. Each set has the smallest power of two greater than or equal to 4 × availableProcessors(): with 24 processors reported to the JVM, that is 128 stripes. Selection mixes the virtual thread's identifier, preserving its association even when it changes carrier threads.
This organization reuses states and SecureRandom instances across short-lived tasks. Threads assigned to the same stripe share generator state and can compete for its lock.
For applications that already organize work in event loops or partition workers, UUIDv7Generator provides an explicit instance. The application controls its lifecycle and maintains the same sequence across single-value calls and batches.
3. Generating a Batch Starts with a Reservation
Single-value generation assembles two long values directly and returns a UUID. For a nonempty batch, the generator reads the clock once, reserves a sequence, and writes into the caller's array.
The reservation method uses lastUnixTsMs for the logical timestamp, randB for the counter, and RAND_B_MASK for its 62-bit limit:
boolean tryReserve(int count, long unixTsMs) {
if (unixTsMs <= lastUnixTsMs && RAND_B_MASK - randB < count) {
return false;
}
advance(unixTsMs);
randB += count - 1L;
return true;
}
The internal method receives count > 0. advance() establishes the first value; the addition moves state to the last reserved value. If the counter was at 100, reserving four values at the same timestamp assigns [101, 104] and leaves state at 104.
The counter's initial value has its two upper bits cleared: B₀ ≤ 2^60 − 1. Since Bmax = 2^62 − 1, at least 3 × 2^60 positions remain available. This accommodates any internal reservation with a positive int size immediately after counter initialization.
In stripes, the lock protects reservation and capture of the initial two halves. Normal buffer writing happens after releasing it:
A: reserve [101, 104] under lock → release → write its buffer
B: reserve [105, 108] under lock → release → write its buffer
Updating state under the lock definitively assigns the interval. B can finish before A, but the ranges are disjoint. The next reservation can advance while the previous batch is still being written.
State updates cost O(1) per batch on the normal path; writing remains O(n). A conceptual model is C_UUID(n) ≈ C_fixed/n + C_write, where fixed cost includes the clock, state selection, reservation, and applicable synchronization. The benchmark does not measure these components individually.
If the observed time does not exceed T and the counter does not have room for the batch, the method returns false without changing state. Filling then generates one UUID at a time; for virtual threads, the stripe lock remains held until completion. If the timestamp can no longer advance either, the call can fail after writing part of the batch; values already used are not reused.
The destination belongs to the caller: long[] uses two elements per UUID and byte[] uses 16 bytes in big-endian order. Reusing these arrays avoids per-identifier objects. The application controls buffer consumption and publication to other threads.
4. Randomness According to Purpose
The fast mode uses a non-cryptographic pseudorandom generator to set the initial values of A and B when the logical timestamp advances. Subsequent calls increment the counter. UUIDs generated from the same state at the same timestamp therefore have related values because they share the same starting point.
For the APIs backed by SecureRandom, each state maintains a 512-byte buffer. Both methods consume values from that same buffer:
| API | Policy | Buffer consumption |
|---|---|---|
secureMonotonicUUID() |
SecureRandom supplies initial values and increments; each increment ranges from 1 to 1,024 |
2 bytes per increment that fits in the counter; 16 to set new initial values |
secureUnorderedUUID() |
Fresh random values in all 74 payload bits for each UUID, with no ordering guarantee | 16 bytes per UUID |
secureRandomUUID() is an alias of secureMonotonicUUID() and shares its sequence. When an increment requires advancing the timestamp and restarting the counter, the buffer supplies bytes for both operations.
The two APIs serve different needs: keeping the sequence increasing or generating the entire payload again. In the monotonic API backed by SecureRandom, the next UUID from the same state, at the same timestamp and without restarting the counter, has at most 1,024 possible values. This sequence is therefore not intended for secret tokens.
5. The Result of Batch Generation
The September 5, 2026 measurements used JMH 1.37, Temurin OpenJDK 25.0.4.1+1-LTS, Windows 11 Pro, and an Intel Core i7-13700K. The protocol used two forks, five warmup and five measurement iterations of one second each, a fixed 1 GiB heap, and -prof gc. The cached clock was disabled.
Single-thread results:
| Output | Million UUIDs/s | Allocation, B/UUID |
|---|---|---|
randomUUID() / object |
260.1 ± 3.9 | 32.00 |
fill(long[]) / batch of 256 |
4,449.3 ± 29.8 | ≈ 0 |
fill(byte[]) / batch of 256 |
4,030.3 ± 33.4 | ≈ 0 |
secureMonotonicUUID() / object |
118.4 ± 6.0 | 32.78 |
secureUnorderedUUID() / object |
27.3 ± 0.4 | 38.25 |
The ± values are the half-widths of the 99.9% confidence intervals reported by JMH. Near-zero allocation refers to generation after warmup into previously allocated, reused arrays.
The long[] batch achieved approximately 17.1 times the per-identifier throughput of the single-value API. This result combines sharing costs across the batch with changing the output format: the batch writes into a reused array containing 4 KiB of data, while the single-value API delivers objects to JMH. The 32 B/UUID measured for that API implies about 8.32 GB/s of allocation at that rate.
With @OperationsPerInvocation(256), JMH counts 256 identifiers per call. The 0.225 ns/UUID derived from throughput is an amortized cost: the batch cost divided by its 256 values. Per batch, the value is approximately 57.5 ns. Single-call latency requires a separate measurement.
With eight platform threads, the fast API reached an aggregate 1.018 billion ± 31.5 million UUIDs/s: approximately 3.91 times single-thread throughput. Throughput increased, with sublinear scaling on this machine.
In another experiment, each invocation submitted 64 virtual-thread tasks, with one thousand UUIDs per task, and waited for their completion. Generation into byte[] reached 1.414 million ± 0.095 million tasks/s, equivalent to about 1.414 billion UUIDs/s. This measurement includes submission, scheduling, value consumption, waiting, and a new array per task; its unit is the complete task.
The numbers describe these environments and consumption patterns. Batch generation shows its advantage when the consumer uses the binary output; object conversion, persistence, and networking add costs absent from the array-filling measurement.
6. Installation with Maven
With Maven and JDK 17 or later, run the following command from the uuidv7 source directory to compile the component, run its tests, and install version 1.3.0 into the local Maven repository:
mvn install "-Dgpg.skip=true"
The -Dgpg.skip=true option disables GPG artifact signing for this local installation.
Then add the dependency inside <dependencies> in the application's pom.xml:
<dependency>
<groupId>io.github.robsonkades</groupId>
<artifactId>uuidv7</artifactId>
<version>1.3.0</version>
</dependency>
7. From the Component to the Data Flow
The API supports starting with single-value generation and using batches where processing already groups records:
import io.github.robsonkades.uuidv7.UUIDv7;
import io.github.robsonkades.uuidv7.UUIDv7Generator;
public class UUIDv7Example {
public static void main(String[] args) {
var id = UUIDv7.randomUUID();
byte[] batch = new byte[16 * 256];
UUIDv7.fill(batch, 0, 256);
// Consume all 256 identifiers before reusing the buffer.
var generator = UUIDv7Generator.create(); // Use from one thread.
var nextId = generator.next();
}
}
A service can use randomUUID() when creating an entity. A pipeline already producing binary blocks can fill its buffer with fill(). A worker with confined state can keep its own UUIDv7Generator instance. All three paths express the same state ownership idea, with representations suited to each consumer.
Given the same initial state and the same clock reading for every element, reservation produces the same sequence as generating one UUID at a time. The tests compare both batch formats with the single-value sequence across 45 combinations of counter, clock, and size. They also check the next output and exercise eight threads sharing a stripe near rollover.
The core idea of uuidv7 is to organize generation around the producer and the destination of the data. Local states allow independent sequences; stripes reuse resources across virtual threads; reservations divide fixed cost across many identifiers. The batch API makes this combination accessible without requiring an intermediate object for every generated value.
Top comments (0)