This package provides a robust and efficient Go implementation of the ULID (Universally Unique Lexicographically Sortable Identifier) specification, as defined in github.com/ulid/spec. ULIDs are designed to be universally unique, lexicographically sortable, and more compact than UUIDs.
- 128-bit Compatibility with UUID: Seamless integration with systems that use UUIDs.
- High Throughput: Generates 1.21e+24 unique ULIDs per millisecond, suitable for high-demand applications.
- Lexicographical Sortability: Enables efficient sorting and indexing in databases and other systems.
- Compact Representation: Encoded as a 26-character string using Crockford's Base32, compared to the 36-character UUID.
- Crockford's Base32 Encoding: Improves readability and efficiency by excluding ambiguous characters (I, L, O, U).
- Lowercase by Default: New in v1.1+ - generates lowercase ULIDs for better readability while maintaining case-insensitive parsing.
- Case Insensitive Parsing: Accepts both uppercase and lowercase ULIDs for backward compatibility.
- URL Safety: Contains no special characters, making it safe for use in URLs and web applications.
- Monotonicity: Ensures correct sorting order even when multiple ULIDs are generated within the same millisecond.
- Thread Safety: Safe for concurrent use in multi-threaded applications.
- High Performance: Optimized implementation with ~10-50x performance improvements over previous versions.
- Zero Dependencies: No external dependencies beyond Go standard library.
ULIDs offer significant advantages over traditional UUIDs and other identifier schemes, making them ideal for modern applications:
Database B-Tree Efficiency:
- Sequential inserts - ULIDs are time-ordered, reducing B-tree fragmentation
- Better cache locality - Related records are stored near each other
- Faster queries - Range queries benefit from natural time-based ordering
- Reduced index rebuilding - Less page splits in database indexes
Generation Speed:
- ~6x faster than UUID v4 - Optimized encoding and no complex formatting
- 151ns per ULID vs typical UUID libraries at 800-1000ns
- Zero allocations for parsing - Efficient byte operations
- Batch generation friendly - Monotonicity allows rapid successive generation
Index Performance:
-- ULID: Natural time-based clustering
SELECT * FROM orders WHERE created_between('01H0', '01H1'); -- Fast range scan
-- UUID: Random distribution requires full index scan
SELECT * FROM orders WHERE created_between(uuid1, uuid2); -- Slower, fragmented
Storage Efficiency:
- 26 characters vs UUID's 36 characters (28% smaller in string form)
- Better compression - Time prefix allows better compression ratios
- Reduced storage I/O - Smaller keys mean more records per page
Human Readable:
ULID: 01ARZ3NDEKTSV4RRFFQ69G5FAV (sortable, readable timestamp prefix)
UUID: 550e8400-e29b-41d4-a716-446655440000 (random, no meaningful order)
Natural Sorting:
- Lexicographic sorting matches chronological order
- No custom comparators needed
- Works in any system - databases, file systems, logs
URL & API Friendly:
- No special characters - safe in URLs without encoding
- Case insensitive - works with case-insensitive systems
- Compact - shorter URLs and API responses
E-commerce Platform Example:
Traditional UUID v4 System:
- Order insertion: ~2000ms for 10K orders (random B-tree splits)
- Recent orders query: ~150ms (index fragmentation)
- Database size: 2.1GB for 1M orders
ULID-Based System:
- Order insertion: ~400ms for 10K orders (sequential inserts)
- Recent orders query: ~25ms (clustered data)
- Database size: 1.8GB for 1M orders (better compression)
Microservices Benefits:
- Distributed tracing - Natural correlation by time
- Log aggregation - Events sort chronologically across services
- Debugging - Easy to spot time-related patterns
- Monitoring - Time-based partitioning works naturally
Collision Resistance:
- 1.21e+24 unique IDs per millisecond - practically impossible collisions
- Cryptographically secure randomness - unpredictable despite time component
- No coordination required - safe in distributed systems
Vs UUID Comparison:
Feature | ULID | UUID v4 | UUID v1 |
---|---|---|---|
Sortable | ✅ Natural | ❌ Random | |
Performance | ✅ ~150ns | ❌ ~800ns | ❌ ~600ns |
Size | ✅ 26 chars | ❌ 36 chars | ❌ 36 chars |
B-tree friendly | ✅ Sequential | ❌ Random | |
URL safe | ✅ No encoding | ❌ Needs encoding | ❌ Needs encoding |
Human readable | ✅ Time prefix | ❌ Opaque | |
Privacy | ✅ Anonymous | ✅ Anonymous | ❌ MAC exposed |
Perfect for:
- High-throughput applications requiring fast inserts
- Time-series data and event logging
- Distributed systems needing correlation
- APIs where shorter IDs improve performance
- Database-heavy applications with frequent queries
Consider alternatives when:
- Existing systems deeply integrated with UUID v4
- Regulatory requirements mandate specific UUID versions
- Time-based correlation is undesired for privacy reasons
A ULID consists of two components:
01AN4Z07BY 79KA1307SR9X4MV3
|-----------| |----------------|
Timestamp Randomness
48bits 80bits
- Timestamp (48 bits): Represents the UNIX timestamp in milliseconds, allowing for time-based sorting and uniqueness. This component provides time representation up to the year 10889 AD.
- Randomness (80 bits): A cryptographically secure random value that ensures uniqueness even within the same millisecond.
To install the ULID
package, use the following command:
go get github.com/cloudresty/ulid
func New() (string, error)
Generates a new ULID string using the current UNIX timestamp in milliseconds.
ulidStr, err := ulid.New()
if err != nil {
// Handle error
}
func NewTime(timestamp uint64) (string, error)
Generates a new ULID string using the provided UNIX timestamp in milliseconds.
ulidStr, err := ulid.NewTime(1678886400000)
if err != nil {
// Handle error
}
func Parse(s string) (ULID, error)
Parses a ULID string and returns a ULID
struct. Returns an error if the string is invalid.
parsedUlid, err := ulid.Parse("01ARZ3NDEKTSV4RRFFQ69G5FAV")
if err != nil {
// Handle error
}
func (u ULID) String() string
Returns the canonical 26-character string representation of the ULID
.
ulidStr := parsedUlid.String()
The package returns errors for:
- Invalid ULID string formats.
- Timestamps exceeding the maximum allowed value.
- Randomness generation failures.
- Randomness overflow during monotonic generation.
The New()
and NewTime()
functions are thread-safe, ensuring safe concurrent use.
To run comprehensive benchmarks and generate a detailed performance report:
cd benchmarks
go run benchmark.go
This will generate a RESULTS.md
file with:
- Detailed performance metrics
- System information
- Optimization techniques used
- Usage examples
- Comparison data
For standard Go benchmarks:
go test -bench=. -benchmem
goos: darwin
goarch: arm64
pkg: github.com/cloudresty/ulid
cpu: Apple M1 Max
BenchmarkNew-10 7565216 151.6 ns/op 32 B/op 1 allocs/op
BenchmarkParse-10 56834553 20.89 ns/op 0 B/op 0 allocs/op
BenchmarkString-10 50176750 24.70 ns/op 32 B/op 1 allocs/op
Run cd benchmarks && go run benchmark.go
to generate fresh benchmark results:
- Generation Rate: ~6.18 million ULIDs/second
- Average Latency: ~161ns per ULID
- Memory Efficiency: 32B/op, 1 alloc/op
- Throughput: 100,000 ULIDs in ~16ms
See benchmarks/RESULTS.md for detailed performance analysis and system information.
package main
import (
"fmt"
"log"
"github.com/cloudresty/ulid"
)
func main() {
// Generate a new ULID
ulidStr, err := ulid.New()
if err != nil {
log.Fatalf("Error generating ULID: %v", err)
}
fmt.Println("Generated ULID:", ulidStr)
// Parse a ULID string
parsedUlid, err := ulid.Parse(ulidStr)
if err != nil {
log.Fatalf("Error parsing ULID: %v", err)
}
fmt.Println("Parsed ULID time:", parsedUlid.GetTime())
// Generate a ULID with a specific timestamp
timestamp := uint64(1678886400000) // Example timestamp (milliseconds)
ulidStr2, err := ulid.NewTime(timestamp)
if err != nil {
log.Fatalf("Error generating ULID with time: %v", err)
}
fmt.Println("ULID with specific timestamp:", ulidStr2)
}
ULIDs are highly versatile and can be used in various applications, including JSON APIs, NoSQL databases, and SQL databases.
package main
import (
"encoding/json"
"fmt"
"log"
"github.com/cloudresty/ulid"
)
type User struct {
ID string `json:"id"`
Name string `json:"name"`
}
func main() {
ulidStr, err := ulid.New()
if err != nil {
log.Fatalf("Error generating ULID: %v", err)
}
user := User{
ID: ulidStr,
Name: "John Doe",
}
userJSON, err := json.Marshal(user)
if err != nil {
log.Fatalf("Error marshaling JSON: %v", err)
}
fmt.Println(string(userJSON))
}
When using MongoDB, you can store ULIDs as strings. MongoDB's indexing and sorting capabilities will work seamlessly with ULIDs.
package main
import (
"context"
"fmt"
"log"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"github.com/cloudresty/ulid"
)
type Product struct {
ID string `bson:"_id"`
Name string `bson:"name"`
}
func main() {
clientOptions := options.Client().ApplyURI("mongodb://localhost:27017")
client, err := mongo.Connect(context.TODO(), clientOptions)
if err != nil {
log.Fatal(err)
}
defer func() {
if err = client.Disconnect(context.TODO()); err != nil {
panic(err)
}
}()
collection := client.Database("testdb").Collection("products")
ulidStr, err := ulid.New()
if err != nil {
log.Fatalf("Error generating ULID: %v", err)
}
product := Product{
ID: ulidStr,
Name: "Laptop",
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err = collection.InsertOne(ctx, product)
if err != nil {
log.Fatal(err)
}
fmt.Println("Product inserted with ID:", product.ID)
// Find the product
var foundProduct Product
err = collection.FindOne(ctx, bson.M{"_id": product.ID}).Decode(&foundProduct)
if err != nil {
log.Fatal(err)
}
fmt.Println("Found product:", foundProduct)
}
ULIDs can also be used as primary keys in SQL databases like PostgreSQL. You can store them as VARCHAR(26)
columns.
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/lib/pq" // PostgreSQL driver
"github.com/cloudresty/ulid"
)
type Order struct {
ID string
UserID int
Amount float64
}
func main() {
connStr := "user=postgres password=password dbname=testdb sslmode=disable"
db, err := sql.Open("postgres", connStr)
if err != nil {
log.Fatal(err)
}
defer db.Close()
ulidStr, err := ulid.New()
if err != nil {
log.Fatalf("Error generating ULID: %v", err)
}
order := Order{
ID: ulidStr,
UserID: 123,
Amount: 99.99,
}
_, err = db.Exec("CREATE TABLE IF NOT EXISTS orders (id VARCHAR(26) PRIMARY KEY, user_id INTEGER, amount FLOAT)")
if err != nil {
log.Fatal(err)
}
_, err = db.Exec("INSERT INTO orders (id, user_id, amount) VALUES ($1, $2, $3)", order.ID, order.UserID, order.Amount)
if err != nil {
log.Fatal(err)
}
fmt.Println("Order inserted with ID:", order.ID)
// Find the order
var foundOrder Order
err = db.QueryRow("SELECT id, user_id, amount FROM orders WHERE id = $1", order.ID).Scan(&foundOrder.ID, &foundOrder.UserID, &foundOrder.Amount)
if err != nil {
log.Fatal(err)
}
fmt.Println("Found order:", foundOrder)
}
ULIDs are perfect for distributed systems where you need globally unique IDs without coordination:
package main
import (
"fmt"
"log"
"sync"
"time"
"github.com/cloudresty/ulid"
)
// Service represents a microservice instance
type Service struct {
ID string
Name string
Requests []Request
mu sync.Mutex
}
type Request struct {
ID string `json:"id"`
ServiceID string `json:"service_id"`
Path string `json:"path"`
Timestamp time.Time `json:"timestamp"`
}
func (s *Service) HandleRequest(path string) {
requestID, err := ulid.New()
if err != nil {
log.Printf("Error generating request ID: %v", err)
return
}
s.mu.Lock()
s.Requests = append(s.Requests, Request{
ID: requestID,
ServiceID: s.ID,
Path: path,
Timestamp: time.Now(),
})
s.mu.Unlock()
fmt.Printf("Service %s handled request %s for %s\n", s.Name, requestID, path)
}
func main() {
serviceID, _ := ulid.New()
service := &Service{
ID: serviceID,
Name: "api-gateway",
}
// Simulate concurrent requests
var wg sync.WaitGroup
paths := []string{"/users", "/orders", "/products", "/health"}
for i := 0; i < 10; i++ {
wg.Add(1)
go func(path string) {
defer wg.Done()
service.HandleRequest(path)
}(paths[i%len(paths)])
}
wg.Wait()
fmt.Printf("Service processed %d requests\n", len(service.Requests))
}
ULIDs provide natural ordering for events in event-driven architectures:
package main
import (
"encoding/json"
"fmt"
"log"
"time"
"github.com/cloudresty/ulid"
)
type Event struct {
ID string `json:"id"`
AggregateID string `json:"aggregate_id"`
Type string `json:"type"`
Data interface{} `json:"data"`
Timestamp time.Time `json:"timestamp"`
Version int `json:"version"`
}
type EventStore struct {
events []Event
}
func (es *EventStore) AppendEvent(aggregateID, eventType string, data interface{}) error {
eventID, err := ulid.New()
if err != nil {
return err
}
event := Event{
ID: eventID,
AggregateID: aggregateID,
Type: eventType,
Data: data,
Timestamp: time.Now(),
Version: len(es.events) + 1,
}
es.events = append(es.events, event)
return nil
}
func (es *EventStore) GetEvents(aggregateID string) []Event {
var events []Event
for _, event := range es.events {
if event.AggregateID == aggregateID {
events = append(events, event)
}
}
return events
}
func main() {
store := &EventStore{}
userID, _ := ulid.New()
// User lifecycle events
store.AppendEvent(userID, "UserCreated", map[string]string{
"email": "[email protected]",
"name": "John Doe",
})
store.AppendEvent(userID, "EmailUpdated", map[string]string{
"old_email": "[email protected]",
"new_email": "[email protected]",
})
store.AppendEvent(userID, "UserDeactivated", map[string]string{
"reason": "User requested account deletion",
})
// Retrieve and display events (naturally ordered by ULID)
events := store.GetEvents(userID)
for _, event := range events {
eventJSON, _ := json.MarshalIndent(event, "", " ")
fmt.Println(string(eventJSON))
fmt.Println("---")
}
}
Perfect for organizing files, documents, and media with time-based sorting:
package main
import (
"fmt"
"path/filepath"
"time"
"github.com/cloudresty/ulid"
)
type Document struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Size int64 `json:"size"`
Path string `json:"path"`
Created time.Time `json:"created"`
Modified time.Time `json:"modified"`
}
type DocumentManager struct {
documents map[string]Document
basePath string
}
func NewDocumentManager(basePath string) *DocumentManager {
return &DocumentManager{
documents: make(map[string]Document),
basePath: basePath,
}
}
func (dm *DocumentManager) CreateDocument(name, docType string, size int64) (*Document, error) {
docID, err := ulid.New()
if err != nil {
return nil, err
}
now := time.Now()
doc := Document{
ID: docID,
Name: name,
Type: docType,
Size: size,
Path: filepath.Join(dm.basePath, docType, docID+filepath.Ext(name)),
Created: now,
Modified: now,
}
dm.documents[docID] = doc
return &doc, nil
}
func (dm *DocumentManager) GetDocument(id string) (*Document, bool) {
doc, exists := dm.documents[id]
return &doc, exists
}
func (dm *DocumentManager) ListDocumentsByType(docType string) []Document {
var docs []Document
for _, doc := range dm.documents {
if doc.Type == docType {
docs = append(docs, doc)
}
}
return docs
}
func main() {
dm := NewDocumentManager("/storage/documents")
// Create various documents
documents := []struct {
name, docType string
size int64
}{
{"report.pdf", "pdf", 1024000},
{"presentation.pptx", "presentation", 2048000},
{"image.jpg", "image", 512000},
{"contract.pdf", "pdf", 256000},
{"logo.png", "image", 128000},
}
for _, d := range documents {
doc, err := dm.CreateDocument(d.name, d.docType, d.size)
if err != nil {
fmt.Printf("Error creating document: %v\n", err)
continue
}
fmt.Printf("Created document: %s (ID: %s)\n", doc.Name, doc.ID)
}
// List all PDF documents (naturally sorted by creation time due to ULID)
pdfs := dm.ListDocumentsByType("pdf")
fmt.Println("\nPDF Documents:")
for _, pdf := range pdfs {
fmt.Printf("- %s (Created: %s, Path: %s)\n",
pdf.Name, pdf.Created.Format(time.RFC3339), pdf.Path)
}
}
ULIDs provide excellent correlation IDs for distributed tracing and logging:
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/cloudresty/ulid"
)
type Logger struct {
serviceName string
}
type LogEntry struct {
TraceID string `json:"trace_id"`
SpanID string `json:"span_id"`
Service string `json:"service"`
Level string `json:"level"`
Message string `json:"message"`
Timestamp time.Time `json:"timestamp"`
}
func (l *Logger) WithTrace(ctx context.Context, message, level string) {
traceID := ctx.Value("trace_id").(string)
spanID, _ := ulid.New()
entry := LogEntry{
TraceID: traceID,
SpanID: spanID,
Service: l.serviceName,
Level: level,
Message: message,
Timestamp: time.Now(),
}
fmt.Printf("[%s] %s | %s | Trace: %s | Span: %s | %s\n",
entry.Timestamp.Format("2006-01-02 15:04:05"),
entry.Level,
entry.Service,
entry.TraceID,
entry.SpanID,
entry.Message)
}
func processOrder(ctx context.Context, orderID string) {
logger := &Logger{serviceName: "order-service"}
logger.WithTrace(ctx, fmt.Sprintf("Processing order %s", orderID), "INFO")
// Simulate processing time
time.Sleep(50 * time.Millisecond)
logger.WithTrace(ctx, "Validating order items", "DEBUG")
time.Sleep(30 * time.Millisecond)
logger.WithTrace(ctx, "Order validation complete", "INFO")
time.Sleep(20 * time.Millisecond)
logger.WithTrace(ctx, fmt.Sprintf("Order %s processed successfully", orderID), "INFO")
}
func main() {
// Create a trace ID for the entire request
traceID, err := ulid.New()
if err != nil {
log.Fatal(err)
}
ctx := context.WithValue(context.Background(), "trace_id", traceID)
// Create an order ID
orderID, _ := ulid.New()
fmt.Printf("Starting request processing with trace ID: %s\n", traceID)
fmt.Println("=" * 80)
processOrder(ctx, orderID)
fmt.Println("=" * 80)
fmt.Printf("Request completed. All logs correlated by trace ID: %s\n", traceID)
}
ULIDs work great for gaming applications where you need unique player/game session IDs:
package main
import (
"fmt"
"math/rand"
"sort"
"time"
"github.com/cloudresty/ulid"
)
type Player struct {
ID string `json:"id"`
Username string `json:"username"`
Score int `json:"score"`
Level int `json:"level"`
}
type GameSession struct {
ID string `json:"id"`
PlayerID string `json:"player_id"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
Score int `json:"score"`
Duration time.Duration `json:"duration"`
}
type GameManager struct {
players map[string]Player
sessions []GameSession
}
func NewGameManager() *GameManager {
return &GameManager{
players: make(map[string]Player),
sessions: make([]GameSession, 0),
}
}
func (gm *GameManager) CreatePlayer(username string) (*Player, error) {
playerID, err := ulid.New()
if err != nil {
return nil, err
}
player := Player{
ID: playerID,
Username: username,
Score: 0,
Level: 1,
}
gm.players[playerID] = player
return &player, nil
}
func (gm *GameManager) StartGameSession(playerID string) (*GameSession, error) {
sessionID, err := ulid.New()
if err != nil {
return nil, err
}
session := GameSession{
ID: sessionID,
PlayerID: playerID,
StartTime: time.Now(),
}
return &session, nil
}
func (gm *GameManager) EndGameSession(session *GameSession) {
session.EndTime = time.Now()
session.Duration = session.EndTime.Sub(session.StartTime)
// Simulate random score
session.Score = rand.Intn(10000) + 100
gm.sessions = append(gm.sessions, *session)
// Update player's total score
if player, exists := gm.players[session.PlayerID]; exists {
player.Score += session.Score
if player.Score > player.Level*1000 {
player.Level++
}
gm.players[session.PlayerID] = player
}
}
func (gm *GameManager) GetTopPlayers(limit int) []Player {
players := make([]Player, 0, len(gm.players))
for _, player := range gm.players {
players = append(players, player)
}
sort.Slice(players, func(i, j int) bool {
return players[i].Score > players[j].Score
})
if limit > len(players) {
limit = len(players)
}
return players[:limit]
}
func main() {
gm := NewGameManager()
// Create players
playerNames := []string{"Alice", "Bob", "Charlie", "Diana", "Eve"}
var players []*Player
for _, name := range playerNames {
player, err := gm.CreatePlayer(name)
if err != nil {
fmt.Printf("Error creating player %s: %v\n", name, err)
continue
}
players = append(players, player)
fmt.Printf("Created player: %s (ID: %s)\n", player.Username, player.ID)
}
// Simulate game sessions
fmt.Println("\n=== Game Sessions ===")
for _, player := range players {
for i := 0; i < 3; i++ { // 3 sessions per player
session, err := gm.StartGameSession(player.ID)
if err != nil {
continue
}
// Simulate game duration
time.Sleep(time.Millisecond * time.Duration(rand.Intn(100)+50))
gm.EndGameSession(session)
fmt.Printf("Session %s: %s scored %d points in %v\n",
session.ID, player.Username, session.Score, session.Duration)
}
}
// Show leaderboard
fmt.Println("\n=== Leaderboard ===")
topPlayers := gm.GetTopPlayers(5)
for i, player := range topPlayers {
fmt.Printf("%d. %s - Level %d - %d points (ID: %s)\n",
i+1, player.Username, player.Level, player.Score, player.ID)
}
}
Contributions are welcome! Please submit pull requests or bug reports through GitHub.
This project is licensed under the MIT License. See the LICENSE file for details.
Made with