Skip to content
This repository was archived by the owner on Dec 1, 2025. It is now read-only.

Commit 707b2d6

Browse files
authored
SECENG-3602: add UUIDv7 methods, deprecate prefixComb methods (#56)
* SECENG-3602: add UUIDv7 methods, depcreate `prefixComb` methods * Add support for deterministic UUIDs * Fix helper functon visibility and Javadoc line length * Update adding constant ordering tests * Clarify long-term intentions WRT UUIDv7 generation code
1 parent 6f11ec3 commit 707b2d6

File tree

4 files changed

+220
-7
lines changed

4 files changed

+220
-7
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.14.0] - 2025-07-10
9+
10+
### Changed
11+
- Added support for generating [UUIDv7](https://uuid7.com/)s (`generateTimePrefixedUuid`).
12+
- Added support for generating UUIDv8s. This implementation (`generateDeterministicTimePrefixedUuid`) is a combination of UUIDv7's time-prefixing and UUIDv3's seeding.
13+
- Deprecated prefix combined UUIDv4s in favour of UUIDv7s or UUIDv8s (as appropriate).
14+
815
## [1.13.4] - 2025-06-17
916

1017
### Changed

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
version=1.13.4
1+
version=1.14.0

tw-base-utils/src/main/java/com/transferwise/common/baseutils/UuidUtils.java

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,75 @@ public static UUID generateSecureUuid() {
2727
return new UUID(applyVersionBits(numberGenerator.nextLong(), V4_VERSION_BITS), numberGenerator.nextLong());
2828
}
2929

30+
/**
31+
* Timestamp-prefixed UUID for where UUIDs should be time-sortable, see <a href="https://uuid7.com/">uuid7.com</a> for details.
32+
*
33+
* <p>This UUID is not suitable for things like session and authentication tokens.
34+
*
35+
* <p>This method is likely to be deprecated once <a href="https://bugs.openjdk.org/browse/JDK-8334015">JDK-8334015</a> has been released, in favour of the built-in UUIDv7 implementation.
36+
*/
37+
public static UUID generateTimePrefixedUuid() {
38+
long timestamp = ClockHolder.getClock().millis();
39+
return generateTimePrefixedUuid(timestamp);
40+
}
41+
42+
/**
43+
* Timestamp-prefixed UUID for where UUIDs should be time-sortable, see <a href="https://uuid7.com/">uuid7.com</a> for details.
44+
*
45+
* <p>This UUID is not suitable for things like session and authentication tokens.
46+
*
47+
* <p>This method is likely to be deprecated once <a href="https://bugs.openjdk.org/browse/JDK-8334015">JDK-8334015</a> has been released, in favour of the built-in UUIDv7 implementation.
48+
*
49+
* @param timestamp provided timestamp milliseconds from epoch.
50+
*/
51+
public static UUID generateTimePrefixedUuid(long timestamp) {
52+
// use built-in implementation once https://bugs.openjdk.org/browse/JDK-8334015 is done and released
53+
byte[] bytes = new byte[16];
54+
numberGenerator.nextBytes(bytes);
55+
applyV7StyleTimestampBits(timestamp, bytes);
56+
applyVersionBits(7, bytes);
57+
applyIetfVariantBits(bytes);
58+
return toUuid(bytes);
59+
}
60+
61+
/**
62+
* Deterministic timestamp-prefixed UUID. This may be useful if <a href="https://transferwise.atlassian.net/wiki/spaces/EKB/pages/3184789016/Consistent+and+idempotent+processing+in+multi-step+flows#Branching-UUIDs">branching UUIDs</a> aren't a viable option.
63+
*
64+
* <p>This UUID is not suitable for things like session and authentication tokens.
65+
*
66+
* @param data a byte array that will be used to generate the UUID.
67+
*/
68+
public static UUID generateDeterministicTimePrefixedUuid(byte[] data) {
69+
long timestamp = ClockHolder.getClock().millis();
70+
return generateDeterministicTimePrefixedUuid(timestamp, data);
71+
}
72+
73+
/**
74+
* Deterministic timestamp-prefixed UUID. This may be useful if <a href="https://transferwise.atlassian.net/wiki/spaces/EKB/pages/3184789016/Consistent+and+idempotent+processing+in+multi-step+flows#Branching-UUIDs">branching UUIDs</a> aren't a viable option.
75+
*
76+
* <p>This UUID is not suitable for things like session and authentication tokens.
77+
*
78+
* @param timestamp provided timestamp milliseconds from epoch.
79+
* @param data a byte array that will be used to generate the UUID.
80+
*/
81+
public static UUID generateDeterministicTimePrefixedUuid(long timestamp, byte[] data) {
82+
byte[] bytes = toBytes(UUID.nameUUIDFromBytes(data));
83+
applyV7StyleTimestampBits(timestamp, bytes);
84+
applyVersionBits(8, bytes);
85+
applyIetfVariantBits(bytes);
86+
return toUuid(bytes);
87+
}
88+
3089
/**
3190
* Random UUID with 38 bit prefix based on current milliseconds from epoch.
3291
*
3392
* <p>Giving about 3181 days roll-over.
3493
*
3594
* <p>This UUID is not suitable for things like session and authentication tokens.
95+
*
96+
* @deprecated in a favour of {@link #generateTimePrefixedUuid()}.
3697
*/
98+
@Deprecated(forRemoval = false)
3799
public static UUID generatePrefixCombUuid() {
38100
return generatePrefixCombUuid(38);
39101
}
@@ -44,7 +106,10 @@ public static UUID generatePrefixCombUuid() {
44106
* <p>Giving about 3181 days roll-over.
45107
*
46108
* <p>This UUID is not suitable for things like session and authentication tokens.
109+
*
110+
* @deprecated in a favour of {@link #generateDeterministicTimePrefixedUuid(long timestamp, byte[] data)}.
47111
*/
112+
@Deprecated(forRemoval = false)
48113
public static UUID generatePrefixCombUuid(long timestamp, UUID uuid) {
49114
return generatePrefixCombUuid(timestamp, uuid, 38);
50115
}
@@ -61,7 +126,11 @@ public static UUID generatePrefixCombUuid(long timestamp, UUID uuid) {
61126
* @param timestamp provided timestamp milliseconds from epoch.
62127
* @param uuid provided uuid.
63128
* @param timePrefixLengthBits technically we left-shift the current time-millis by that amount.
129+
*
130+
* @deprecated in a favour of {@link #generateDeterministicTimePrefixedUuid(long timestamp, byte[] data)}.
131+
* Note that the replacement method has a fixed 48-bit timestamp prefix.
64132
*/
133+
@Deprecated(forRemoval = false)
65134
public static UUID generatePrefixCombUuid(long timestamp, UUID uuid, int timePrefixLengthBits) {
66135
if (timePrefixLengthBits < 1 || timePrefixLengthBits > 63) {
67136
throw new IllegalArgumentException("Prefix length " + timePrefixLengthBits + " has to be between 1 and 63, inclusively.");
@@ -82,7 +151,10 @@ public static UUID generatePrefixCombUuid(long timestamp, UUID uuid, int timePre
82151
* <p>This UUID is not suitable for things like session and authentication tokens.
83152
*
84153
* @param timePrefixLengthBits technically we left-shift the current time-millis by that amount.
154+
*
155+
* @deprecated in a favour of {@link #generateTimePrefixedUuid()}. Note that UUIDv7s have a fixed 48-bit timestamp prefix.
85156
*/
157+
@Deprecated(forRemoval = false)
86158
public static UUID generatePrefixCombUuid(int timePrefixLengthBits) {
87159
if (timePrefixLengthBits < 1 || timePrefixLengthBits > 63) {
88160
throw new IllegalArgumentException("Prefix length " + timePrefixLengthBits + " has to be between 1 and 63, inclusively.");
@@ -135,8 +207,29 @@ public static UUID add(UUID uuid, long constant) {
135207
return new UUID(uuid.getMostSignificantBits(), uuid.getLeastSignificantBits() + constant);
136208
}
137209

138-
protected static long applyVersionBits(final long msb, int versionBits) {
210+
private static void applyV7StyleTimestampBits(long timestamp, byte[] bytes) {
211+
bytes[0] = (byte)(timestamp >>> 40);
212+
bytes[1] = (byte)(timestamp >>> 32);
213+
bytes[2] = (byte)(timestamp >>> 24);
214+
bytes[3] = (byte)(timestamp >>> 16);
215+
bytes[4] = (byte)(timestamp >>> 8);
216+
bytes[5] = (byte)(timestamp);
217+
}
218+
219+
private static long applyVersionBits(final long msb, int versionBits) {
139220
return (msb & 0xffffffffffff0fffL) | versionBits;
140221
}
141222

223+
private static void applyVersionBits(int version, byte[] bytes) {
224+
// Set version bits
225+
bytes[6] &= 0x0f;
226+
bytes[6] |= (byte) (version << 4);
227+
}
228+
229+
private static void applyIetfVariantBits(byte[] bytes) {
230+
// Set variant to IETF
231+
bytes[8] &= 0x3f;
232+
bytes[8] |= (byte) 0x80;
233+
}
234+
142235
}

tw-base-utils/src/test/java/com/transferwise/common/baseutils/UuidUtilsTest.java

Lines changed: 118 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import com.google.common.collect.Ordering;
1111
import com.transferwise.common.baseutils.clock.TestClock;
12+
import java.nio.charset.StandardCharsets;
1213
import java.time.Duration;
1314
import java.time.Instant;
1415
import java.util.Arrays;
@@ -34,24 +35,107 @@ void defaultPrefixCombUuidIsGrowingOverTime() {
3435

3536
long previousTime = -1;
3637
for (int i = 0; i < n; i++) {
38+
System.out.println("Testing " + uuids[i]);
39+
assertEquals(4, uuids[i].version());
3740
long time = uuids[i].getMostSignificantBits() >>> (64 - 38);
3841

3942
if (previousTime != -1) {
4043
assertTrue(previousTime < time);
4144
}
4245
previousTime = time;
46+
}
4347

44-
System.out.println(uuids[i]);
45-
assertEquals(4, uuids[i].version());
48+
assertTrue(Ordering.natural().isOrdered(Arrays.asList(uuids)));
49+
assertEquals(n, Set.of(uuids).size());
50+
}
51+
52+
@Test
53+
void timePrefixedUuidIsVersion7() {
54+
UUID uuid = UuidUtils.generateTimePrefixedUuid();
55+
assertEquals(7, uuid.version());
56+
}
57+
58+
@Test
59+
void timePrefixedUuidIsGrowingOverTime() {
60+
TestClock clock = TestClock.createAndRegister();
61+
62+
int n = 100;
63+
64+
UUID[] uuids = new UUID[n];
65+
for (int i = 0; i < n; i++) {
66+
uuids[i] = UuidUtils.generateTimePrefixedUuid();
67+
clock.tick(Duration.ofMillis(2));
68+
}
69+
70+
long previousTime = -1;
71+
for (int i = 0; i < n; i++) {
72+
System.out.println("Testing " + uuids[i]);
73+
long time = uuids[i].getMostSignificantBits() >>> (64 - 48);
74+
75+
if (previousTime != -1) {
76+
assertTrue(previousTime < time);
77+
}
78+
previousTime = time;
79+
}
80+
81+
assertTrue(Ordering.natural().isOrdered(Arrays.asList(uuids)));
82+
assertEquals(n, Set.of(uuids).size());
83+
}
84+
85+
@Test
86+
void deterministicTimePrefixedUuidIsVersion8() {
87+
UUID uuid = UuidUtils.generateDeterministicTimePrefixedUuid(new byte[0]);
88+
System.out.println(uuid);
89+
assertEquals(8, uuid.version());
90+
}
91+
92+
@Test
93+
void deterministicTimePrefixedUuidIsGrowingOverTime() {
94+
TestClock clock = TestClock.createAndRegister();
95+
96+
int n = 100;
97+
98+
UUID[] uuids = new UUID[n];
99+
for (int i = 0; i < n; i++) {
100+
uuids[i] = UuidUtils.generateDeterministicTimePrefixedUuid(new byte[0]);
101+
clock.tick(Duration.ofMillis(2));
102+
}
103+
104+
long previousTime = -1;
105+
for (int i = 0; i < n; i++) {
106+
System.out.println("Testing " + uuids[i]);
107+
long time = uuids[i].getMostSignificantBits() >>> (64 - 48);
108+
109+
if (previousTime != -1) {
110+
assertTrue(previousTime < time);
111+
}
112+
previousTime = time;
46113
}
47114

48115
assertTrue(Ordering.natural().isOrdered(Arrays.asList(uuids)));
49116
assertEquals(n, Set.of(uuids).size());
50117
}
51118

119+
@Test
120+
void deterministicTimePrefixedUuidIsFullyDeterministic() {
121+
long timestamp = System.currentTimeMillis();
122+
byte[] seed = "Ben was here".getBytes(StandardCharsets.UTF_8);
123+
UUID uuid1 = UuidUtils.generateDeterministicTimePrefixedUuid(timestamp, seed);
124+
UUID uuid2 = UuidUtils.generateDeterministicTimePrefixedUuid(timestamp, seed);
125+
assertEquals(uuid1, uuid2);
126+
}
127+
128+
@Test
129+
void deterministicTimePrefixedUuidChangesWithDifferentData() {
130+
long timestamp = System.currentTimeMillis();
131+
UUID uuid1 = UuidUtils.generateDeterministicTimePrefixedUuid(timestamp, "Ben was here".getBytes(StandardCharsets.UTF_8));
132+
UUID uuid2 = UuidUtils.generateDeterministicTimePrefixedUuid(timestamp, "Ben was not here".getBytes(StandardCharsets.UTF_8));
133+
assertNotEquals(uuid1, uuid2);
134+
}
135+
52136
@Test
53137
void constantCanBeAddedToUuid() {
54-
var uuid = UuidUtils.generatePrefixCombUuid();
138+
var uuid = UuidUtils.generateTimePrefixedUuid();
55139
var uuidWithConstant = UuidUtils.add(uuid, 11111);
56140

57141
assertNotEquals(uuidWithConstant, uuid);
@@ -60,7 +144,7 @@ void constantCanBeAddedToUuid() {
60144
}
61145

62146
@Test
63-
void addingConstantDoesNotChangeOrdering() {
147+
void addingConstantDoesNotChangeOrderingOfPrefixCombUuid() {
64148
TestClock clock = TestClock.createAndRegister();
65149

66150
int n = 100;
@@ -73,7 +157,7 @@ void addingConstantDoesNotChangeOrdering() {
73157

74158
long previousTime = -1;
75159
for (int i = 0; i < n; i++) {
76-
long time = uuids[i].getMostSignificantBits() >>> (64 - 38);
160+
long time = uuids[i].getMostSignificantBits() >>> (64 - 48);
77161

78162
if (previousTime != -1) {
79163
assertTrue(previousTime < time);
@@ -88,6 +172,35 @@ void addingConstantDoesNotChangeOrdering() {
88172
assertEquals(n, Set.of(uuids).size());
89173
}
90174

175+
@Test
176+
void addingConstantDoesNotChangeOrderingOfTimePrefixedUuid() {
177+
TestClock clock = TestClock.createAndRegister();
178+
179+
int n = 100;
180+
181+
UUID[] uuids = new UUID[n];
182+
for (int i = 0; i < n; i++) {
183+
uuids[i] = UuidUtils.add(UuidUtils.generateTimePrefixedUuid(), ThreadLocalRandom.current().nextInt());
184+
clock.tick(Duration.ofMillis(2));
185+
}
186+
187+
long previousTime = -1;
188+
for (int i = 0; i < n; i++) {
189+
long time = uuids[i].getMostSignificantBits() >>> (64 - 48);
190+
191+
if (previousTime != -1) {
192+
assertTrue(previousTime < time);
193+
}
194+
previousTime = time;
195+
196+
System.out.println(uuids[i]);
197+
assertEquals(7, uuids[i].version());
198+
}
199+
200+
assertTrue(Ordering.natural().isOrdered(Arrays.asList(uuids)));
201+
assertEquals(n, Set.of(uuids).size());
202+
}
203+
91204
@Test
92205
void convertingFromUuidAndBackToBytesEndWithTheSameResult() {
93206
UUID expected = UUID.randomUUID();

0 commit comments

Comments
 (0)