UUID di Microservices & CockroachDB: Antara Skalabilitas dan Performa
Di dunia microservices modern, UUID sering dipilih sebagai primary key (PK) hampir tanpa pikir panjang. Alasannya terdengar masuk akal: UUID unik secara global, aman untuk distributed system, dan menghindari koordinasi antar service. Namun ketika sistem mulai scale, banyak tim baru menyadari satu hal pahit:
Query join jadi lambat. Sangat lambat.
Bahkan bisa puluhan hingga ratusan kali lebih lambat dibanding integer ID.
Artikel ini membahas secara mendalam:
- Apa itu UUID dan kenapa populer sebagai PK
- Kenapa ukurannya besar tapi tetap dipilih
- Apa yang sebenarnya salah ketika performa drop
- Dampaknya di database distributed seperti CockroachDB
- Best practice yang benar-benar dipakai di sistem skala besar
Apa Itu UUID?
UUID (Universally Unique Identifier) adalah identifier 128-bit (16 byte) yang dirancang agar unik tanpa koordinasi pusat.
Contoh UUID:
550e8400-e29b-41d4-a716-446655440000
Varian UUID yang umum
- UUID v4: random
- UUID v1: time + MAC address (jarang dipakai sekarang)
- UUID v7: time-ordered (standar baru, sangat relevan untuk database)
UUID biasanya direpresentasikan sebagai string, tapi di database disimpan sebagai binary 16 byte.
Varian UUID dan Identifier Serupa
Tidak semua UUID diciptakan sama. Banyak masalah performa yang dibahas di artikel ini bukan karena UUID itu sendiri, melainkan karena varian UUID yang dipilih.
Berikut varian UUID dan identifier populer yang sering digunakan di sistem modern, beserta ringkasan karakteristiknya.
UUID v4 (Random UUID)
Karakteristik:
- 100% random
- Tidak memiliki urutan waktu
- Paling umum dipakai secara default
Kelebihan:
- Sangat mudah digunakan
- Tidak bocor informasi waktu atau urutan
- Aman untuk exposure publik
Kekurangan (krusial):
- Sangat buruk untuk index locality
- Menyebabkan fragmentasi index
- Join mahal, terutama di database besar dan distributed
Ringkasan:
Cocok untuk identifier eksternal, buruk sebagai primary key join-heavy.
UUID v1 (Time-based UUID)
Karakteristik:
- Berbasis timestamp + node identifier
- Bersifat time-ordered
Kelebihan:
- Index lebih teratur dibanding v4
- Insert lebih sequential
Kekurangan:
- Berpotensi membocorkan informasi waktu dan node
- Sudah jarang direkomendasikan
Ringkasan:
Lebih baik dari v4 untuk database, tapi kurang ideal untuk sistem modern.
UUID v7 (Time-ordered UUID)
Karakteristik:
- Standar UUID terbaru
- Timestamp di bagian awal
- Tetap unik secara global
Kelebihan:
- Sangat baik untuk index locality
- Insert hampir sequential
- Cocok untuk distributed SQL (CockroachDB, Spanner)
Kekurangan:
- Belum tersedia native di semua database lama
Ringkasan:
Pilihan UUID terbaik saat ini untuk primary key database.
ULID (Universally Lexicographically Sortable Identifier)
Karakteristik:
- 128-bit (setara UUID)
- Lexicographically sortable
- Biasanya direpresentasikan sebagai string base32
Kelebihan:
- Time-ordered
- Mudah dibaca manusia
- Sangat baik untuk index
Kekurangan:
- Bukan standar UUID resmi
- Perlu konsistensi implementasi
Ringkasan:
Alternatif UUID v7 yang sangat populer di microservices.
KSUID (K-Sortable Unique Identifier)
Karakteristik:
- 160-bit
- Timestamp + random payload
- Dirancang oleh Segment
Kelebihan:
- Sangat baik untuk sistem event dan log
- Sorting alami berdasarkan waktu
Kekurangan:
- Lebih besar dari UUID
- Index lebih berat
Ringkasan:
Cocok untuk event stream, kurang ideal sebagai PK relasional.
Ringkasan Cepat Perbandingan
| Identifier | Size | Ordered | Cocok untuk PK | Cocok untuk Join |
|---|---|---|---|---|
| UUID v4 | 128-bit | ❌ | ⚠️ | ❌ |
| UUID v1 | 128-bit | ✅ | ⚠️ | ⚠️ |
| UUID v7 | 128-bit | ✅ | ✅ | ✅ |
| ULID | 128-bit | ✅ | ✅ | ✅ |
| KSUID | 160-bit | ✅ | ⚠️ | ⚠️ |
Kenapa UUID Banyak Digunakan Sebagai Primary Key?
UUID menjadi populer karena menjawab banyak masalah klasik di sistem terdistribusi.
1. Cocok untuk microservices
- Tidak perlu auto-increment global
- Service bisa generate ID sendiri
- Aman untuk async processing dan event-driven architecture
2. Aman untuk exposure ke publik
- Tidak mudah ditebak
- Tidak bocor urutan data
- Cocok untuk API publik
3. Mudah untuk merge data
- Tidak ada bentrok ID saat import / migrasi
- Aman untuk multi-region dan multi-cluster
Semua ini membuat UUID terlihat seperti pilihan paling rasional.
Tapi… Kenapa Ukuran Besar Tetap Dipilih?
Walaupun besar, banyak engineer menganggap:
- “Storage murah”
- “CPU sekarang cepat”
- “Index pasti di-handle database”
Masalahnya: bukan cuma soal storage.
Yang terdampak adalah:
- Struktur index
- Cache locality
- Network traffic (di distributed DB)
- Join strategy
Efeknya baru terasa saat data besar dan query kompleks.
Contoh Kasus Nyata
Your microservices use UUID primary keys. When you try to join data from two services, queries are 100x slower than with integer IDs.
Contoh query:
SELECT *
FROM orders o
JOIN payments p ON o.id = p.order_id;
Dengan integer ID: cepat. Dengan UUID: tiba-tiba query time meledak.
Kenapa?
Apa yang Sebenarnya Terjadi di Balik Layar?
1. UUID itu besar dan mahal untuk dibandingkan
- BIGINT: 8 byte
- UUID: 16 byte
Setiap perbandingan:
- Lebih banyak CPU instruction
- Lebih banyak cache miss
Dalam join besar, ini terakumulasi.
2. UUID random merusak index locality
Terutama UUID v4:
- Insert masuk ke posisi acak di B-tree
- Index sering split
- Fragmentasi meningkat
Akibatnya:
- Scan index lebih mahal
- Join makin lambat
3. Di CockroachDB, dampaknya lebih besar
CockroachDB adalah distributed SQL database:
- Data dibagi ke banyak node
- Index dipartisi menjadi range
- Join bisa lintas node
UUID v4 menyebabkan:
- Range churn
- Banyak RPC antar node
- Distributed hash join yang mahal
Jadi masalahnya bukan cuma CPU, tapi network + koordinasi.
UUID memperbesar Raft & replication cost
Karena CockroachDB menggunakan Raft:
- Key lebih besar → log lebih besar
- Replikasi lebih berat
- Recovery lebih lambat
Ini sering tidak disadari, tapi terasa di scale.
Kesalahan Umum yang Sering Terjadi
❌ Menggunakan UUID v4 sebagai PK default
Tanpa mempertimbangkan efek index dan join.
❌ Menganggap UUID selalu “best practice”
UUID adalah tradeoff, bukan solusi universal.
❌ Join lintas service saat runtime
Ini melanggar prinsip microservices dan memperparah masalah performa.
❌ Menggunakan BIGSERIAL polos di CockroachDB
Ini bisa menyebabkan hotspot write.
Best Practice yang Direkomendasikan
✅ Gunakan UUID yang time-ordered
Di CockroachDB (dan database modern):
- UUID v7
- ULID
- KSUID
Contoh:
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT uuid_v7(),
...
);
Manfaat:
- Insert hampir sequential
- Index lebih stabil
- Join jauh lebih efisien
✅ Pisahkan ID eksternal dan ID internal
Pola umum di sistem besar:
orders (
id BIGINT PRIMARY KEY,
public_id UUID UNIQUE
)
public_id: untuk API & antar serviceid: untuk join internal
Ini sering jadi kompromi terbaik.
✅ Hindari join lintas service
Gunakan:
- Denormalized read model
- CQRS
- Event-driven projection
- Data warehouse untuk analytics
Join runtime lintas domain = bom waktu performa.
✅ Desain PK sesuai database
Di CockroachDB:
- Hindari auto-increment murni
- Hindari UUID random
- Pilih key yang seimbang antara write dan read
Penutup
UUID bukanlah kesalahan desain.
Kesalahannya adalah:
- Menggunakan UUID random tanpa mempertimbangkan index
- Menggunakannya sebagai satu-satunya join key
- Mengabaikan sifat database distributed
Di CockroachDB dan microservices skala besar, UUID v7 atau ULID adalah pilihan realistis, bukan UUID v4.
UUID tetap relevan — asal digunakan dengan sadar.