diving in to Leach-Salz Version 4 UUIDs

Written on May 8, 2025

An UUID is a 128-bit value used to uniquely identify objects or entities in computer systems. The structure and interpretation of a UUID depend on its variant and version fields.

UUIDs (Universally Unique Identifiers) are essential in distributed systems for generating unique identifiers without central coordination. Among the various UUID versions, Leach-Salz version 4 (also known as random UUIDs) is one of the most widely used. In this post, we’ll explore how they work, their implementation, and their real-world applications.

Leach-Salz (Variant 2) UUIDs

A Leach-Salz (Variant 2) version 4 UUID is a randomly generated UUID that follows the UUID version 4 specification. It’s named after its creators, Paul Leach and Rich Salz.

Variant 2, also known as the Leach-Salz variant, is the most common UUID layout, standardized by the IETF in RFC 4122. In this variant:

  • The variant field occupies three bits and is set to the bit pattern 10x (where x can be 0 or 1).

  • The layout of the UUID is as follows:

Field Bits Description
time_low 32 Lower part of timestamp
time_mid 16 Middle part of timestamp
time_hi 12 High part of timestamp
version 4 UUID version
variant 3 UUID variant
clock_seq 13 Clock sequenceNo
node 48 Node identifier
  • The variant value of 2 indicates the Leach-Salz layout, which is used by most modern UUID libraries (including Java’s java.util.UUID and Kafka’s Uuid class)

Structure

xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx

Where:

  • 4 indicates version 4
  • y is one of 8, 9, A, or B (indicating variant 2)
  • x is any hexadecimal digit (0-9 or a-f)

How the Variant Is Encoded

The variant of an UUID is determined by the value of the 17th hexadecimal digit in the UUID string (the first digit of the fourth group). This digit is sometimes called the N digit in the pattern:

xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx

M: Version (e.g., 4 for version 4)

N: Variant

The variant is encoded in the upper bits of this digit. For the Leach-Salz variant (RFC 4122/DCE 1.1), the variant bits are 10x (where x can be 0 or 1).

Why a Means Leach-Salz

Binary for hex a: a in hexadecimal is 1010 in binary.

Variant pattern: The Leach-Salz variant requires the upper two bits to be 10. The next bit (x) can be 0 or 1, and the last bit is not part of the variant.

So, for a (1010):

First two bits: 10 — matches the Leach-Salz variant.

Third bit: 1 — can be anything (the x in 10x).

Fourth bit: Not used for variant.

This is why a in the fourth group means the UUID is of the Leach-Salz variant

Take away

  • Leach-Salz variant: The first two bits of the 17th hex digit are 10.
  • Hex a: Is 1010 in binary, so the first two bits are 10 — matching the Leach-Salz pattern.
  • Result: Any UUID with 8, 9, a, or b in the first position of the fourth group is a Leach-Salz variant UUID.

Implementation in Java

Let’s look at how Java implements UUID generation:

import java.util.UUID;
import java.security.SecureRandom;

public class UUIDExample {
    public static void main(String[] args) {
        // Basic UUID generation
        UUID uuid = UUID.randomUUID();
        System.out.println("Random UUID: " + uuid);
        
        // Using SecureRandom for better randomness
        SecureRandom secureRandom = new SecureRandom();
        byte[] randomBytes = new byte[16];
        secureRandom.nextBytes(randomBytes);
        
        // Set version (4) and variant bits
        randomBytes[6] &= 0x0f;  // Clear version
        randomBytes[6] |= 0x40;  // Set version to 4
        randomBytes[8] &= 0x3f;  // Clear variant
        randomBytes[8] |= 0x80;  // Set variant to Leach-Salz (10x)
        
        UUID secureUUID = constructUUID(randomBytes);
        System.out.println("Secure UUID: " + secureUUID);
    }
    
    private static UUID constructUUID(byte[] data) {
        long msb = 0;
        long lsb = 0;
        
        for (int i = 0; i < 8; i++) {
            msb = (msb << 8) | (data[i] & 0xff);
        }
        for (int i = 8; i < 16; i++) {
            lsb = (lsb << 8) | (data[i] & 0xff);
        }
        
        return new UUID(msb, lsb);
    }
}

How It Works

  1. Random Number Generation:
    • 16 random bytes are generated
    • These bytes form the basis of the UUID
  2. Version and Variant Bits:
    • Version 4 is indicated by setting bits 4-7 of byte 6 to 0100
    • Variant 2 is indicated by setting bits 6-7 of byte 8 to 10 (hex 8, 9, A, or B)
  3. Formatting:
    • The 16 bytes are formatted into the standard UUID string format
    • Hyphens are inserted at specific positions

Comparison with Other UUID Versions

Version 1 (Time-based)

// Time-based UUID (Version 1)
UUID timeBasedUUID = UUID.fromString("123e4567-e89b-11d3-a456-426614174000");
  • Uses timestamp and node ID
  • Predictable and potentially traceable
  • Not recommended for security-sensitive applications

Version 3 (MD5 Hash)

// MD5 Hash-based UUID (Version 3)
UUID namespaceUUID = UUID.fromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8");
UUID name = "example.com";
UUID v3UUID = UUID.nameUUIDFromBytes(name.getBytes());
  • Uses MD5 hash of a namespace and name
  • Deterministic (same input produces same UUID)
  • Useful for generating consistent UUIDs from names

Version 5 (SHA-1 Hash)

// SHA-1 Hash-based UUID (Version 5)
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] hash = md.digest(name.getBytes());
  • Similar to Version 3 but uses SHA-1
  • More secure than Version 3
  • Still deterministic

Real-World Applications

1. Database Primary Keys

@Entity
public class User {
    @Id
    @GeneratedValue(generator = "UUID")
    @GenericGenerator(
        name = "UUID",
        strategy = "org.hibernate.id.UUIDGenerator"
    )
    private UUID id;
    
    private String name;
    // ... other fields
}

2. Distributed Systems

public class DistributedLock {
    private final UUID lockId;
    
    public DistributedLock() {
        this.lockId = UUID.randomUUID();
    }
    
    public boolean acquireLock() {
        // Use lockId as unique identifier for lock
        return distributedLockService.acquire(lockId);
    }
}

3. File Systems

public class FileStorage {
    public String storeFile(byte[] data) {
        UUID fileId = UUID.randomUUID();
        String path = fileId.toString() + ".dat";
        // Store file with UUID as name
        return path;
    }
}

4. Message Queues

public class Message {
    private final UUID messageId;
    private final String content;
    
    public Message(String content) {
        this.messageId = UUID.randomUUID();
        this.content = content;
    }
}

Best Practices

  1. Use SecureRandom for Generation:
    public class SecureUUIDGenerator {
     private static final SecureRandom secureRandom = new SecureRandom();
        
     public static UUID generateSecureUUID() {
         byte[] randomBytes = new byte[16];
         secureRandom.nextBytes(randomBytes);
         // Set version and variant bits
         randomBytes[6] &= 0x0f;
         randomBytes[6] |= 0x40;
         randomBytes[8] &= 0x3f;
         randomBytes[8] |= 0x80;
         return constructUUID(randomBytes);
     }
    }
    
  2. Handle UUID Parsing:
    public class UUIDUtils {
     public static UUID parseUUID(String uuidString) {
         try {
             return UUID.fromString(uuidString);
         } catch (IllegalArgumentException e) {
             throw new InvalidUUIDException("Invalid UUID format: " + uuidString);
         }
     }
    }
    
  3. Performance Considerations:
    public class UUIDCache {
     private static final int CACHE_SIZE = 1000;
     private static final Queue<UUID> uuidCache = new ConcurrentLinkedQueue<>();
        
     static {
         // Pre-generate UUIDs
         for (int i = 0; i < CACHE_SIZE; i++) {
             uuidCache.offer(UUID.randomUUID());
         }
     }
        
     public static UUID getUUID() {
         UUID uuid = uuidCache.poll();
         if (uuid == null) {
             uuid = UUID.randomUUID();
         }
         return uuid;
     }
    }
    

Common Pitfalls

  1. Not Using SecureRandom:
    // Bad: Using Math.random()
    public static UUID generateWeakUUID() {
     byte[] randomBytes = new byte[16];
     for (int i = 0; i < 16; i++) {
         randomBytes[i] = (byte) (Math.random() * 256);
     }
     return constructUUID(randomBytes);
    }
    
  2. Incorrect Version/Variant Bits:
    // Bad: Not setting version and variant bits
    public static UUID generateInvalidUUID() {
     byte[] randomBytes = new byte[16];
     new SecureRandom().nextBytes(randomBytes);
     return constructUUID(randomBytes); // Missing version/variant bits
    }
    

Conclusion

Leach-Salz version 4 UUIDs are a crucial tool in distributed systems, providing a reliable way to generate unique identifiers without central coordination. Their random nature makes them suitable for security-sensitive applications, while their standardized format ensures compatibility across systems.

When implementing UUIDs in your system:

  • Use SecureRandom for generation
  • Properly set version and variant bits
  • Consider performance implications
  • Handle parsing errors gracefully
  • Use appropriate storage types in databases

References

  1. RFC 4122 - A Universally Unique IDentifier (UUID) URN Namespace
  2. Java UUID Documentation
  3. Hibernate UUID Generation Strategies