UUID di Microservices & CockroachDB: Antara Skalabilitas dan Performa
Di dunia microservices, UUID sering dipilih sebagai primary key hampir tanpa pikir panjang. Alasannya terdengar masuk akal — UUID unik secara global, bisa di-generate tanpa koordinasi pusat, dan aman untuk sistem terdistribusi. Namun ketika sistem mulai scale dan data tumbuh, banyak tim baru menyadari satu hal pahit: query join yang dulu terasa cepat tiba-tiba bisa ratusan kali lebih lambat dibanding sistem yang menggunakan integer ID. Masalahnya bukan UUID itu sendiri, melainkan varian UUID yang dipilih dan bagaimana ia berinteraksi dengan struktur index database — terutama di distributed SQL seperti CockroachDB.
Apa Itu UUID?
UUID (Universally Unique Identifier) adalah identifier 128-bit (16 byte) yang dirancang agar unik tanpa membutuhkan koordinasi pusat. Tidak ada server tunggal yang perlu dihubungi untuk memastikan ID tidak bentrok — setiap service bisa generate UUID-nya sendiri.
Contoh UUID: 550e8400-e29b-41d4-a716-446655440000
Di database, UUID biasanya disimpan sebagai binary 16 byte meskipun sering terlihat sebagai string. Ada beberapa varian UUID, dan pilihan varian inilah yang paling menentukan nasib performa sistemmu.
Varian UUID yang Relevan
UUID v4 adalah yang paling umum dipakai secara default — sepenuhnya random, tidak memiliki urutan waktu sama sekali. Mudah digunakan dan tidak bocor informasi temporal, tapi justru sifat randomnya inilah yang menjadi akar masalah performa.
UUID v1 berbasis timestamp dan node identifier sehingga bersifat time-ordered. Lebih baik dari v4 untuk performa database, tapi berpotensi membocorkan informasi waktu dan identitas node. Sudah jarang direkomendasikan di sistem modern.
UUID v7 adalah standar terbaru — menggabungkan timestamp di bagian awal dengan random bits di bagian belakang. Insert hampir sequential, index jauh lebih stabil, dan tetap unik secara global. Inilah yang seharusnya dipakai sebagai primary key di database modern.
Kenapa UUID Populer Sebagai Primary Key?
UUID menjawab beberapa masalah nyata di arsitektur terdistribusi yang sulit diselesaikan dengan auto-increment integer.
Tidak perlu koordinasi pusat. Dengan BIGSERIAL atau auto-increment, setiap insert harus menghubungi satu titik untuk mendapat ID berikutnya. Di microservices, ini menciptakan bottleneck dan coupling. UUID bisa di-generate di sisi aplikasi, bahkan sebelum menyentuh database.
Aman untuk API publik. Integer ID yang sequential mudah ditebak — attacker bisa enumerate resource hanya dengan menambah angka. UUID tidak memiliki pola yang bisa diprediksi, sehingga aman untuk diekspos ke luar.
Tidak ada bentrok saat merge data. Saat migasi, import, atau sinkronisasi data antar cluster, UUID hampir mustahil bentrok secara kebetulan. Ini sangat berharga di sistem multi-region.
Semua alasan ini valid. UUID bukan pilihan yang salah — hanya sering dipakai tanpa memahami trade-off-nya.
Apa yang Sebenarnya Terjadi Saat Performa Drop?
Skenario ini sering terjadi di tim yang baru scale:
-- Query yang kelihatannya wajar
SELECT o.*, p.*
FROM orders o
JOIN payments p ON o.id = p.order_id;
-- Dengan integer ID: selesai dalam milidetik
-- Dengan UUID v4: bisa butuh detik, bahkan puluhan detik di data besar
Ada tiga mekanisme yang bekerja bersamaan sehingga UUID v4 begitu mahal untuk join.
1. Ukuran Perbandingan yang Lebih Besar
BIGINT hanya 8 byte. UUID 16 byte — dua kali lipat. Dalam setiap join, database harus membandingkan key dari satu tabel dengan key di tabel lain. Di join besar dengan jutaan baris, perbedaan 8 byte per perbandingan terakumulasi menjadi perbedaan yang signifikan di CPU dan cache.
2. UUID v4 Merusak Index Locality
Ini adalah masalah yang paling fundamental. Database modern menggunakan B-tree untuk index. B-tree bekerja optimal ketika insert datang secara terurut — node baru selalu ditambahkan di ujung, tidak di tengah.
UUID v4 sepenuhnya random. Setiap insert baru masuk ke posisi acak di B-tree, memaksa database untuk terus melakukan page split dan reorganisasi struktur index.
flowchart TD
subgraph Integer["Insert dengan Integer ID (Sequential)"]
A1[1] --> A2[2] --> A3[3] --> A4[4] --> A5[5]
style Integer fill:#d4edda
end
subgraph UUID4["Insert dengan UUID v4 (Random)"]
B1[550e...]
B2[a3f1...]
B3[12bc...]
B4[ff09...]
B1 -.->|"split paksa"| B3
B3 -.->|"split paksa"| B2
B2 -.->|"split paksa"| B4
style UUID4 fill:#f8d7da
endHasilnya adalah index yang terfragmentasi — saat database perlu scan index untuk join, ia harus melompat-lompat ke halaman yang tersebar di seluruh disk, bukan membaca secara linear.
3. Dampak Berlipat di CockroachDB
CockroachDB adalah distributed SQL database — data dibagi ke banyak node dalam bentuk range (kumpulan key yang berdekatan). Join di CockroachDB bukan hanya operasi di satu mesin, tapi bisa melibatkan RPC antar node.
UUID v4 yang random menyebabkan range churn — data tersebar merata ke seluruh node tanpa pola, sehingga setiap join bisa membutuhkan koordinasi ke banyak node sekaligus.
sequenceDiagram
participant Client
participant Node1
participant Node2
participant Node3
Client->>Node1: JOIN orders + payments
Note over Node1: UUID v4: data tersebar random
Node1->>Node2: Fetch key a3f1... (ada di node lain)
Node1->>Node3: Fetch key ff09... (ada di node lain)
Node2-->>Node1: Result
Node3-->>Node1: Result
Node1-->>Client: Response (lambat karena banyak RPC)Di atas join cost, ada juga biaya replikasi yang lebih tinggi karena CockroachDB menggunakan Raft — key yang lebih besar berarti log Raft yang lebih besar, dan recovery yang lebih lambat.
Perbandingan Identifier untuk Primary Key
Tidak semua UUID diciptakan sama. Berikut perbandingan lengkap opsi yang relevan.
| Identifier | Ukuran | Time-ordered | Cocok untuk PK | Efisiensi Join |
|---|---|---|---|---|
| UUID v4 | 128-bit | ✗ | ⚠️ Buruk untuk skala besar | ✗ Buruk |
| UUID v1 | 128-bit | ✓ | ⚠️ Lebih baik, tapi ada risiko privasi | ⚠️ Sedang |
| UUID v7 | 128-bit | ✓ | ✓ Sangat baik | ✓ Baik |
| ULID | 128-bit | ✓ | ✓ Sangat baik | ✓ Baik |
| KSUID | 160-bit | ✓ | ⚠️ Cocok untuk event stream | ⚠️ Sedang |
| BIGINT | 64-bit | ✓ | ✓ Terbaik untuk join | ✓ Terbaik |
UUID v7 dan ULID adalah titik tengah yang ideal untuk microservices: tetap unik secara global tanpa koordinasi, tapi time-ordered sehingga insert sequential dan index stabil.
flowchart TD
A{Perlu unik secara global\ntanpa koordinasi?} -- Ya --> B{Perlu diekspos\nke API publik?}
A -- Tidak --> C[BIGINT / BIGSERIAL]
B -- Ya --> D{Database modern\nUUID v7 support?}
B -- Tidak --> E[UUID v7 sebagai PK\nBIGINT sebagai internal ID]
D -- Ya --> F[UUID v7]
D -- Tidak --> G[ULID]Anti-Pattern yang Harus Dihindari
-- ✗ Anti-pattern 1: UUID v4 sebagai PK di tabel yang sering di-join
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- v4, fully random
user_id UUID,
...
);
-- ✓ Solusi: gunakan UUID v7 atau ULID agar insert sequential
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT uuid_v7(),
user_id UUID,
...
);
-- ✗ Anti-pattern 2: satu UUID untuk semua keperluan sekaligus
-- (PK internal, join key, dan identifier publik API jadi satu)
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
-- id ini dipakai untuk join, untuk URL, untuk semua hal
);
-- ✓ Solusi: pisahkan ID internal dan ID publik
CREATE TABLE users (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, -- untuk join internal
public_id UUID UNIQUE DEFAULT uuid_v7() -- untuk API eksternal
);
-- ✗ Anti-pattern 3: BIGSERIAL polos di CockroachDB
CREATE TABLE events (
id BIGSERIAL PRIMARY KEY -- sequential insert → hotspot di satu node
);
-- ✓ Solusi: gunakan UUID v7 atau hash-sharded index
CREATE TABLE events (
id UUID PRIMARY KEY DEFAULT uuid_v7() -- distribusi lebih merata
);
-- ✗ Anti-pattern 4: join lintas service saat runtime
SELECT o.*, u.name
FROM orders_service.orders o
JOIN users_service.users u ON o.user_id = u.id; -- join database lintas domain
-- ✓ Solusi: gunakan denormalized read model atau event-driven projection
-- Simpan data yang dibutuhkan di domain sendiri, bukan join ke domain lain
Best Practice di Sistem Produksi
Gunakan UUID v7 atau ULID sebagai Primary Key
Keduanya time-ordered sehingga insert hampir sequential — index tidak terfragmentasi, dan join jauh lebih efisien dibanding UUID v4.
-- CockroachDB dengan UUID v7
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT uuid_v7(),
created_at TIMESTAMPTZ DEFAULT now(),
customer_id UUID NOT NULL,
total_amount DECIMAL(12,2)
);
-- Atau dengan ULID di aplikasi (generate di sisi app, simpan sebagai string)
CREATE TABLE orders (
id TEXT PRIMARY KEY, -- ULID dari aplikasi
created_at TIMESTAMPTZ DEFAULT now()
);
Pisahkan ID Internal dan ID Publik
Pola ini dipakai di sistem skala besar untuk mendapat yang terbaik dari keduanya — performa join dari integer, dan keamanan UUID untuk API publik.
CREATE TABLE products (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
-- ^ digunakan untuk semua join internal, sangat efisien
public_id UUID UNIQUE NOT NULL DEFAULT uuid_v7(),
-- ^ digunakan di URL, API response, dan komunikasi antar service
name TEXT NOT NULL,
price DECIMAL(10,2)
);
Dengan pola ini, query join internal menggunakan id (BIGINT), sementara API eksternal dan komunikasi antar service menggunakan public_id (UUID). Performa join tidak terganggu, keamanan tetap terjaga.
Hindari Join Lintas Domain di Runtime
Ini adalah prinsip microservices yang sering diabaikan demi kenyamanan jangka pendek. Join database lintas service domain adalah bom waktu — skalanya buruk, coupling-nya tinggi, dan sangat sulit diperbaiki di kemudian hari.
flowchart LR
subgraph Buruk["❌ Join Runtime Lintas Domain"]
A[Order Service] -->|JOIN saat query| B[User Service DB]
A -->|JOIN saat query| C[Product Service DB]
end
subgraph Baik["✓ Read Model / Projection"]
D[Order Service] --> E[Order Read Model]
F[User Service] -->|event: user.updated| E
G[Product Service] -->|event: product.updated| E
H[Query] --> E
endGunakan CQRS, denormalized read model, atau event-driven projection untuk data yang perlu dikombinasikan lintas domain. Analytics berat baiknya dialihkan ke data warehouse, bukan query runtime ke database operasional.
Checklist Primary Key di Sistem Terdistribusi
PEMILIHAN IDENTIFIER:
□ UUID v7 atau ULID dipilih jika butuh keunikan global tanpa koordinasi
□ UUID v4 TIDAK digunakan sebagai PK di tabel yang sering di-join
□ BIGINT dipertimbangkan untuk tabel internal yang tidak perlu diekspos ke luar
□ BIGSERIAL TIDAK digunakan di CockroachDB (risiko hotspot write)
DESAIN TABEL:
□ ID internal (BIGINT) dan ID publik (UUID) dipisahkan jika perlu keduanya
□ Index pada foreign key menggunakan kolom yang sama dengan PK yang direferensikan
□ Tidak ada cross-domain join di query runtime
UNTUK COCKROACHDB KHUSUSNYA:
□ PK dipilih yang mendistribusikan write secara merata antar node
□ UUID v7 atau hash-sharded index digunakan untuk tabel besar
□ Range splits dimonitor untuk mendeteksi hotspot
MONITORING:
□ Query plan diperiksa untuk memastikan index digunakan dengan efisien
□ Slow query log diaktifkan dan dimonitor
□ Metrik index fragmentation dipantau secara berkala
Ringkasan
- UUID bukan masalah, UUID v4 sebagai PK join-heavy yang jadi masalah — sifat randomnya merusak index locality dan menyebabkan fragmentasi B-tree yang signifikan di skala besar.
- UUID v7 dan ULID adalah pilihan terbaik saat ini untuk primary key di database modern — keduanya time-ordered sehingga insert sequential, index stabil, dan join efisien.
- Di CockroachDB, masalahnya berlipat — UUID v4 menyebabkan range churn dan banyak RPC antar node, karena data join tersebar ke seluruh cluster tanpa pola.
- Pisahkan ID internal dan ID publik jika butuh keduanya: BIGINT untuk join internal yang cepat, UUID sebagai
public_iduntuk API dan komunikasi antar service.- BIGSERIAL polos berbahaya di CockroachDB — sequential insert menyebabkan semua write menumpuk di satu node (hotspot), mengalahkan tujuan distribusi cluster.
- Hindari join lintas domain di runtime — gunakan read model, CQRS, atau event-driven projection. Cross-service join adalah anti-pattern microservices sekaligus bom waktu performa.
- UUID tetap relevan dan valid — kuncinya adalah memilih varian yang tepat dan menggunakannya untuk tujuan yang tepat, bukan menghindarinya sepenuhnya.