Database Sharding: Arsitektur, Trade-off, dan Kapan Kompleksitasnya Tidak Sepadan
Ada titik di mana satu server database, tidak peduli seberapa besar spesifikasinya, akhirnya mentok. Write throughput sudah maksimal, disk I/O jadi bottleneck, dan menambah RAM atau CPU lagi tidak lagi memberi peningkatan performa yang sepadan dengan biayanya. Di titik ini, sharding sering muncul sebagai solusi yang terdengar menjanjikan — pecah database jadi banyak instance kecil, masing-masing menangani sebagian data, beban jadi terdistribusi. Tapi sharding adalah salah satu keputusan arsitektur paling berat konsekuensinya dalam rekayasa sistem backend. Ia menyelesaikan masalah skala dengan cara menukar kesederhanaan operasional dengan kompleksitas yang tersebar di hampir setiap lapisan sistem — dari cara aplikasi menulis query, cara transaksi dijamin konsisten, sampai cara tim melakukan maintenance sehari-hari. Artikel ini membahas sharding secara mendalam: bagaimana ia bekerja, masalah nyata yang muncul setelah diterapkan, dan yang terpenting, kapan kompleksitas itu benar-benar sepadan dengan manfaatnya — dan kapan tidak.
Apa itu Sharding
Sharding adalah teknik membagi data ke banyak instance database yang independen, di mana masing-masing instance — disebut shard — menyimpan sebagian data dengan skema yang identik, tapi berjalan sebagai server terpisah yang tidak berbagi resource fisik dengan shard lainnya. Aplikasi (atau layer routing di antaranya) bertanggung jawab menentukan shard mana yang harus dituju untuk operasi tertentu.
flowchart TD
A[Aplikasi] --> B[Router / Shard Map]
B --> C[Shard 1 - Server Database Terpisah]
B --> D[Shard 2 - Server Database Terpisah]
B --> E[Shard 3 - Server Database Terpisah]Ini berbeda mendasar dari partitioning yang sudah dibahas di artikel sebelumnya. Partitioning membagi tabel besar menjadi potongan-potongan fisik, tapi semuanya tetap berada dalam satu instance database yang sama, dikelola oleh satu query planner, dan bisa di-query dalam satu transaksi ACID tanpa masalah koordinasi jaringan. Sharding melangkah lebih jauh: setiap shard adalah database yang benar-benar berdiri sendiri, punya memory, disk, dan proses-nya sendiri. Konsekuensinya, kemudahan yang kamu dapat gratis dari satu instance database — transaksi lintas data, join antar tabel, foreign key constraint — tidak lagi tersedia begitu saja lintas shard.
| Aspek | Partitioning | Sharding |
|---|---|---|
| Lokasi data | Satu instance database | Banyak instance/server terpisah |
| Dikelola oleh | Satu query planner | Masing-masing shard independen |
| Transaksi lintas bagian | Native, dalam satu transaksi | Butuh distributed transaction |
| Kompleksitas ditanggung oleh | Database engine | Aplikasi/middleware routing |
Kenapa Sharding Dibutuhkan
Sebelum membahas cara kerjanya, penting memahami masalah spesifik yang membuat sharding jadi pilihan yang masuk akal — karena masalah ini sering disalahpahami.
Batas Vertical Scaling
Vertical scaling — menambah CPU, RAM, atau disk pada server yang sama — punya batas fisik dan batas ekonomi. Ada ukuran instance tertinggi yang tersedia dari cloud provider manapun, dan biaya instance besar biasanya tidak naik linear; instance dua kali lebih besar seringkali berharga lebih dari dua kali lipat. Selain itu, beberapa bottleneck seperti disk I/O atau network throughput tidak selalu terselesaikan hanya dengan menambah RAM atau CPU.
Batas Write Throughput yang Tidak Terselesaikan Read Replica
Read replica adalah solusi umum untuk skala baca — kamu bisa menambah replica sebanyak yang dibutuhkan untuk melayani lebih banyak query SELECT. Tapi write tetap harus melalui satu instance primary. Kalau bottleneck sistem kamu ada di sisi tulis — misalnya aplikasi dengan volume transaksi sangat tinggi seperti sistem pembayaran atau logging skala besar — menambah read replica sama sekali tidak membantu. Di sinilah sharding jadi relevan: dengan memecah data ke banyak shard, beban tulis ikut terdistribusi karena setiap shard menerima subset write yang berbeda.
Kapan Sharding Jadi Satu-satunya Opsi Realistis
Sharding layak dipertimbangkan serius ketika kombinasi berikut terjadi: volume data atau throughput sudah melampaui kapasitas praktis satu server (bukan sekadar “suatu saat nanti akan besar”), partitioning saja sudah diterapkan tapi tetap tidak cukup karena satu instance masih jadi bottleneck, dan tim sudah mengevaluasi opsi lain yang lebih murah secara operasional namun tetap tidak mencukupi.
flowchart TD
A[Bottleneck performa] --> B{Vertical scaling masih memungkinkan?}
B -- Ya --> C[Upgrade instance]
B -- Tidak --> D{Read replica menyelesaikan masalah?}
D -- Ya, masalah di sisi baca --> E[Tambah read replica]
D -- Tidak, masalah di sisi tulis --> F{Partitioning saja cukup?}
F -- Ya --> G[Terapkan partitioning]
F -- Tidak, instance tetap bottleneck --> H[Pertimbangkan Sharding]Strategi Pembagian Data (Sharding Key)
Kolom atau atribut yang menentukan shard mana yang menampung data tertentu disebut sharding key (atau shard key). Pemilihan sharding key adalah keputusan paling menentukan dalam desain sharding — nyaris semua masalah performa dan operasional yang dibahas di bagian-bagian berikutnya berakar dari pilihan ini.
Range-based Sharding
Data dibagi berdasarkan rentang nilai sharding key, mirip range partitioning tapi setiap rentang berada di server terpisah.
Shard 1: user_id 1 - 1.000.000
Shard 2: user_id 1.000.001 - 2.000.000
Shard 3: user_id 2.000.001 - 3.000.000
Kelebihannya, range query (misalnya “semua user yang mendaftar bulan ini” kalau sharding key-nya tanggal registrasi) bisa diarahkan ke shard yang tepat tanpa perlu menyentuh shard lain. Kelemahannya, distribusi beban bisa sangat timpang — kalau user baru selalu mendapat ID berurutan, shard yang menampung rentang ID terbaru akan menerima mayoritas traffic tulis, sementara shard lama jadi relatif sepi. Ini disebut hot shard.
Hash-based Sharding
Sharding key di-hash, lalu hasil hash menentukan shard tujuan — mirip hash partitioning tapi diterapkan lintas server.
shard_index = hash(user_id) % jumlah_shard
Distribusi data jadi jauh lebih merata dibanding range-based, karena hash function dirancang untuk menyebar nilai secara acak tidak peduli pola aslinya. Trade-off-nya, range query jadi tidak mungkin dilakukan secara efisien — karena data yang berdekatan secara logis (misalnya user dengan ID berurutan) tersebar ke shard yang berbeda-beda secara acak, query untuk mengambil rentang nilai berarti harus menyentuh hampir semua shard.
Directory-based (Lookup) Sharding
Alih-alih menghitung shard tujuan dari rumus tertentu, sistem menyimpan lookup table terpisah yang memetakan setiap sharding key ke shard spesifik.
Lookup Table:
tenant_id=101 -> Shard 2
tenant_id=102 -> Shard 1
tenant_id=103 -> Shard 3
Pendekatan ini memberi fleksibilitas paling besar — kamu bisa memindahkan satu entitas spesifik (misalnya satu tenant besar) ke shard khusus tanpa mengubah rumus hash secara keseluruhan. Tapi lookup table itu sendiri jadi komponen kritis yang harus tersedia (highly available) dan cepat diakses, karena setiap operasi database harus melalui pengecekan lookup terlebih dahulu — ia berpotensi jadi single point of failure atau bottleneck baru kalau tidak didesain dengan baik.
Geo-based Sharding
Data dibagi berdasarkan lokasi geografis pengguna atau region operasional, dengan setiap shard ditempatkan secara fisik lebih dekat dengan penggunanya.
Shard Asia Tenggara: user dengan region = 'SEA'
Shard Eropa: user dengan region = 'EU'
Shard Amerika: user dengan region = 'US'
Selain manfaat distribusi beban, pendekatan ini juga mengurangi latency karena data lebih dekat secara fisik dengan penggunanya, dan bisa membantu kepatuhan regulasi data lokal (data residency) di beberapa yurisdiksi yang mensyaratkan data warga negaranya disimpan di dalam batas negara tertentu.
| Strategi | Distribusi Beban | Range Query | Fleksibilitas | Risiko Utama |
|---|---|---|---|---|
| Range-based | Berpotensi timpang | Efisien | Rendah | Hot shard pada rentang aktif |
| Hash-based | Merata | Tidak efisien (scatter) | Rendah | Sulit migrasi tanpa consistent hashing |
| Directory-based | Fleksibel, terkontrol | Tergantung desain | Tinggi | Lookup table jadi bottleneck/SPOF |
| Geo-based | Tergantung distribusi pengguna | Efisien per region | Sedang | Beban tidak merata antar region |
Arsitektur Routing — Bagaimana Aplikasi Tahu Harus ke Shard Mana
Setelah sharding key dan strategi ditentukan, pertanyaan berikutnya adalah: komponen mana yang bertanggung jawab menerjemahkan sharding key menjadi keputusan “harus connect ke server yang mana”.
Client-side Routing
Logika penentuan shard ditanam langsung di kode aplikasi atau library database client. Aplikasi menghitung shard tujuan sendiri sebelum membuka koneksi.
// Ilustrasi logika di application layer
function getShardConnection(userId) {
const shardIndex = hash(userId) % TOTAL_SHARDS;
return shardConnections[shardIndex];
}
Pendekatan ini sederhana untuk diimplementasikan pada awalnya dan tidak menambah komponen infrastruktur baru, tapi logika sharding jadi tersebar di setiap service yang mengakses database — kalau ada perubahan strategi sharding, semua service itu harus diubah dan di-deploy ulang secara terkoordinasi.
Proxy / Middleware Routing
Sebuah layer terpisah — proxy database seperti Vitess, ProxySQL, atau Citus (untuk PostgreSQL) — berdiri di antara aplikasi dan shard-shard, menerima query seolah-olah itu satu database tunggal, lalu menerjemahkan dan meneruskannya ke shard yang tepat.
sequenceDiagram
participant App as Aplikasi
participant Proxy as Sharding Proxy/Middleware
participant S1 as Shard 1
participant S2 as Shard 2
App->>Proxy: SELECT * FROM orders WHERE user_id = 555
Proxy->>Proxy: Hitung shard tujuan dari user_id
Proxy->>S2: Teruskan query ke Shard yang tepat
S2-->>Proxy: Hasil query
Proxy-->>App: Hasil queryPendekatan ini memusatkan logika sharding di satu tempat, sehingga aplikasi bisa tetap menulis query seolah-olah berbicara dengan satu database. Perubahan strategi sharding, penambahan shard baru, atau migrasi data bisa ditangani di layer proxy tanpa mengubah kode aplikasi. Trade-off-nya, proxy ini menambah satu hop jaringan tambahan (latency) dan menjadi komponen infrastruktur kritis baru yang perlu di-maintain, di-monitor, dan dibuat highly available.
Shard Map sebagai Metadata Tersentral
Baik client-side maupun proxy-based routing pada akhirnya butuh sumber kebenaran tentang pemetaan shard — disebut shard map atau metadata service. Untuk strategi hash-based dengan jumlah shard tetap, shard map bisa sesederhana rumus modulo. Untuk directory-based atau sistem yang sudah pernah melalui resharding, shard map biasanya berupa service atau tabel metadata tersendiri yang harus selalu konsisten dan cepat diakses oleh seluruh komponen yang melakukan routing.
Masalah yang Muncul Setelah Sharding
Bagian ini adalah inti dari kompleksitas sesungguhnya sharding — masalah-masalah yang tidak ada di database single-instance, dan sering diremehkan sebelum sharding benar-benar diterapkan.
Cross-shard Query dan Join
Ketika data yang dibutuhkan satu query tersebar di lebih dari satu shard, JOIN dalam pengertian SQL tradisional tidak lagi bisa dilakukan secara native — karena database engine di satu shard tidak punya akses langsung ke data fisik di shard lain.
-- ✗ Query ini valid di database single-instance, tapi TIDAK bisa dijalankan
-- langsung lintas shard kalau orders dan pelanggan ada di shard berbeda
SELECT o.id, p.nama
FROM orders o
JOIN pelanggan p ON p.id = o.pelanggan_id
WHERE o.created_at >= '2026-01-01';
Ada beberapa strategi untuk mengatasi ini, masing-masing dengan trade-off:
Denormalisasi. Simpan data yang biasanya butuh join (misalnya nama pelanggan) langsung di tabel orders sebagai kolom duplikat, sehingga query tidak perlu join lintas shard sama sekali. Trade-off-nya adalah kompleksitas menjaga konsistensi data yang terduplikasi ketika data sumber berubah.
Scatter-gather query. Aplikasi mengirim query yang sama ke semua shard secara paralel, lalu menggabungkan hasilnya di application layer.
// Ilustrasi scatter-gather di application layer
const hasilSemuaShard = await Promise.all(
semuaShardConnections.map(shard => shard.query('SELECT * FROM orders WHERE status = ?', ['pending']))
);
const hasilGabungan = hasilSemuaShard.flat();
Pendekatan ini fleksibel tapi mahal — kamu kehilangan manfaat sharding sepenuhnya untuk query jenis ini, karena tetap harus menyentuh semua shard, ditambah overhead menggabungkan dan mengurutkan hasil secara manual di kode aplikasi.
Co-location by design. Rancang sharding key sedemikian rupa sehingga data yang sering di-join selalu berada di shard yang sama. Misalnya kalau sharding key adalah tenant_id, pastikan seluruh tabel yang berelasi dengan satu tenant (orders, order_items, invoices) memakai tenant_id yang sama sebagai bagian dari kunci mereka, sehingga semuanya otomatis berada di shard yang sama dan join lokal tetap bisa dilakukan.
flowchart LR
subgraph Shard A
A1[orders tenant_id=101]
A2[pelanggan tenant_id=101]
end
subgraph Shard B
B1[orders tenant_id=102]
B2[pelanggan tenant_id=102]
endCo-location adalah pendekatan yang paling direkomendasikan ketika pola akses aplikasi memungkinkan, karena ia mempertahankan kemampuan join lokal tanpa mengorbankan manfaat distribusi beban dari sharding.
Distributed Transaction
Di database single-instance, transaksi ACID adalah kemewahan yang didapat gratis. Begitu data tersebar lintas shard, menjamin bahwa sekumpulan operasi tulis di beberapa shard sekaligus berhasil semua atau gagal semua (atomicity) jadi masalah yang jauh lebih rumit.
Two-phase commit (2PC) adalah protokol klasik untuk ini: koordinator meminta semua shard yang terlibat untuk “siap commit” (phase 1), dan hanya setelah semua shard menyatakan siap, koordinator mengirim perintah commit final ke semuanya (phase 2). Kalau ada satu shard yang gagal di phase 1, semua shard diminta rollback.
sequenceDiagram
participant Coord as Koordinator Transaksi
participant S1 as Shard 1
participant S2 as Shard 2
Coord->>S1: Prepare commit
Coord->>S2: Prepare commit
S1-->>Coord: Siap
S2-->>Coord: Siap
Coord->>S1: Commit
Coord->>S2: Commit2PC menjamin konsistensi kuat, tapi punya kelemahan signifikan: kalau koordinator crash di antara phase 1 dan phase 2, shard yang sudah menyatakan “siap” bisa terjebak menunggu tanpa tahu harus commit atau rollback, mengunci resource selama itu terjadi. Selain itu, 2PC menambah latency signifikan karena butuh dua round-trip komunikasi penuh sebelum transaksi selesai.
Saga pattern adalah alternatif yang lebih umum dipakai di sistem terdistribusi modern. Alih-alih satu transaksi atomik, operasi dipecah jadi serangkaian langkah lokal di masing-masing shard, dan setiap langkah punya operasi kompensasi (compensating action) untuk membatalkan efeknya kalau langkah berikutnya gagal.
Langkah 1: Kurangi stok produk di Shard Produk -- berhasil
Langkah 2: Buat order di Shard Order -- berhasil
Langkah 3: Proses pembayaran di Shard Payment -- GAGAL
Kompensasi:
Batalkan order di Shard Order
Kembalikan stok produk di Shard Produk
Saga pattern menghindari resource locking jangka panjang ala 2PC, tapi mengorbankan strong consistency demi eventual consistency — ada jeda waktu di mana sistem berada dalam state yang belum sepenuhnya konsisten, dan aplikasi harus dirancang untuk toleran terhadap kondisi ini.
Trade-off antara strong consistency dan availability lintas node terdistribusi ini adalah inti dari CAP theorem — dalam sistem terdistribusi yang mengalami partisi jaringan, kamu harus memilih antara konsistensi penuh atau ketersediaan penuh, tidak bisa keduanya secara simultan. Sharding, sebagai sistem multi-node, tidak kebal dari trade-off ini, dan keputusan desain seperti 2PC vs saga pada dasarnya adalah keputusan di mana kamu memposisikan diri dalam spektrum tersebut.
Rebalancing dan Resharding
Distribusi data antar shard jarang tetap ideal selamanya. Satu tenant bisa tumbuh jauh lebih besar dari yang lain, satu region bisa mengalami lonjakan traffic musiman, atau jumlah shard awal ternyata tidak lagi mencukupi seiring pertumbuhan data — situasi yang disebut hot shard ketika satu shard menerima beban tidak proporsional dibanding yang lain.
Menangani ini berarti memindahkan sebagian data dari satu shard ke shard lain, atau menambah jumlah shard secara keseluruhan — proses yang disebut resharding. Ini adalah salah satu operasi paling berisiko dalam sistem sharded, karena harus dilakukan idealnya tanpa downtime pada sistem yang sedang melayani traffic produksi.
Pendekatan naif — menghitung ulang hash(key) % jumlah_shard_baru — akan menyebabkan hampir seluruh data berpindah shard sekaligus, karena mengubah jumlah shard mengubah hasil modulo untuk hampir semua key. Ini membuat resharding jadi operasi migrasi besar-besaran yang mahal dan berisiko tinggi.
Consistent hashing adalah teknik yang dirancang khusus untuk meminimalkan masalah ini. Alih-alih memetakan key langsung ke nomor shard lewat modulo, baik shard maupun key dipetakan ke titik-titik dalam sebuah lingkaran hash (hash ring). Setiap key ditempatkan di shard terdekat searah jarum jam dari posisinya di ring. Ketika satu shard baru ditambahkan, ia hanya “mengambil alih” sebagian kecil dari ring di sekitarnya — data yang perlu dipindah jauh lebih sedikit dibanding pendekatan modulo naif.
flowchart TD
A[Hash Ring] --> B[Shard 1 - mengelola sebagian ring]
A --> C[Shard 2 - mengelola sebagian ring]
A --> D[Shard 3 Baru - hanya ambil sebagian kecil dari Shard 1 dan 2]Proses resharding produksi biasanya dilakukan bertahap: data disalin ke shard tujuan sambil shard sumber tetap melayani traffic (dual-write atau replikasi), lalu setelah data tersinkron penuh, traffic dialihkan ke shard baru, dan barulah data lama di shard sumber dihapus. Setiap tahap ini butuh mekanisme verifikasi konsistensi yang matang untuk memastikan tidak ada data yang hilang atau duplikat selama proses berlangsung.
Auto-increment ID dan Keunikan Global
Di database single-instance, primary key auto-increment bekerja tanpa masalah karena hanya ada satu sumber penghitung. Begitu ada banyak shard yang masing-masing punya mekanisme auto-increment sendiri-sendiri, dua baris di shard berbeda bisa dengan mudah mendapat ID yang sama persis — jelas bermasalah kalau ID itu diharapkan unik secara global di seluruh sistem.
-- ✗ Auto-increment lokal di tiap shard menghasilkan ID yang bentrok antar shard
-- Shard 1: order dengan id=1001
-- Shard 2: order dengan id=1001 (bentrok!)
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
...
);
Beberapa solusi umum untuk masalah ini:
UUID. Menghasilkan identifier acak yang secara praktis tidak akan pernah bentrok tanpa perlu koordinasi antar shard sama sekali. Kelemahannya, UUID versi acak penuh (v4) tidak berurutan, yang bisa merugikan performa index karena insert baru tersebar acak di struktur B-tree alih-alih menambah di ujung — masalah yang bisa diatasi dengan UUID v7 yang menyertakan komponen timestamp sehingga tetap semi-berurutan.
Snowflake ID. Skema ID yang dipopulerkan oleh Twitter/X, menggabungkan timestamp, ID mesin/shard, dan sequence number lokal dalam satu angka 64-bit. Hasilnya tetap berurutan secara kasar berdasarkan waktu (baik untuk performa index), unik secara global tanpa koordinasi terpusat, dan bisa di-generate secara independen oleh masing-masing shard.
Struktur Snowflake ID (ilustrasi):
[41 bit timestamp][10 bit machine/shard id][12 bit sequence number]
ID generator terpusat. Sebuah service khusus yang bertanggung jawab men-generate ID unik untuk seluruh sistem, biasanya dengan mengalokasikan blok/range ID ke setiap shard secara berkala supaya tidak perlu round-trip ke service ini di setiap insert. Trade-off-nya, service ini jadi komponen kritis baru yang harus selalu tersedia.
Performa: Manfaat dan Biaya Sharding
Manfaat yang Didapat
Manfaat paling nyata dari sharding adalah horizontal write scaling — kemampuan menambah kapasitas tulis sistem secara nyaris linear dengan menambah jumlah shard, sesuatu yang tidak bisa dicapai read replica maupun vertical scaling. Manfaat lain yang sering kurang disorot adalah isolasi blast radius: kalau satu shard mengalami masalah (disk penuh, query runaway, crash), shard lain tetap bisa melayani traffic normal, berbeda dengan database single-instance di mana satu masalah bisa melumpuhkan seluruh sistem sekaligus.
Biaya yang Harus Ditanggung
Latency tambahan dari routing layer. Baik client-side maupun proxy-based routing menambah satu langkah komputasi atau satu hop jaringan sebelum query benar-benar sampai ke shard tujuan — kecil untuk satu query, tapi terasa akumulasinya pada sistem dengan volume request sangat tinggi.
Kompleksitas query bergeser ke aplikasi. Query yang dulunya cukup satu JOIN SQL sederhana di database single-instance, sekarang mungkin butuh scatter-gather manual, penggabungan hasil, dan sorting ulang di application layer — kompleksitas yang dulunya ditangani otomatis oleh query optimizer, sekarang jadi tanggung jawab kode aplikasi yang harus ditulis dan di-maintain manual.
Overhead operasional berlipat. Monitoring, backup, patching, dan tuning sekarang harus dilakukan untuk N instance database, bukan satu. Insiden yang dulunya butuh investigasi di satu tempat sekarang bisa jadi butuh korelasi log dan metrik dari beberapa shard sekaligus untuk memahami akar masalah, terutama kalau masalahnya bersifat lintas shard seperti kegagalan distributed transaction.
Sharding vs Alternatif Lain
Sebelum memutuskan sharding, penting membandingkannya dengan opsi lain yang jauh lebih murah secara operasional.
| Opsi | Menyelesaikan Skala Baca | Menyelesaikan Skala Tulis | Kompleksitas Operasional | Kapan Cukup |
|---|---|---|---|---|
| Vertical scaling | Ya, sampai batas hardware | Ya, sampai batas hardware | Rendah | Volume belum mendekati batas praktis satu server |
| Read replica | Ya, bisa ditambah sesuai kebutuhan | Tidak | Sedang | Bottleneck ada di sisi baca, bukan tulis |
| Partitioning | Sebagian, lewat query yang lebih efisien | Sebagian, index maintenance lebih ringan | Sedang | Tabel besar dalam satu server masih bisa ditangani, butuh organisasi bukan distribusi |
| Sharding | Ya, hampir tanpa batas | Ya, hampir tanpa batas | Tinggi | Volume/throughput sudah melampaui kapasitas praktis satu instance |
Urutan eskalasi yang masuk akal secara umum: mulai dari vertical scaling karena paling sederhana, tambahkan read replica kalau bottleneck ada di sisi baca, terapkan partitioning kalau satu tabel sudah terlalu besar untuk dikelola efisien dalam satu instance, dan baru pertimbangkan sharding kalau kombinasi semua opsi di atas tetap tidak mencukupi kapasitas tulis atau volume data yang dibutuhkan.
Kapan Sebaiknya Pakai Sharding
PAKAI sharding jika:
✓ write throughput sudah melampaui kapasitas praktis satu instance database
✓ vertical scaling, read replica, dan partitioning sudah dicoba dan tetap tidak cukup
✓ ada sharding key alami yang memungkinkan co-location data yang sering diakses bersama
✓ tim punya kapasitas engineering untuk membangun dan memaintain routing layer, monitoring
multi-shard, dan strategi resharding jangka panjang
✓ karakteristik data cocok untuk isolasi (misal multi-tenant dengan tenant_id sebagai sharding key)
Kapan Kompleksitasnya Tidak Sepadan
Karena judul artikel ini secara eksplisit menyoroti soal ini, section ini pantas dibahas lebih dalam ketimbang sekadar daftar poin.
Sharding diterapkan terlalu dini, sebelum benar-benar dibutuhkan. Ini adalah kesalahan paling umum. Tim yang optimis soal pertumbuhan produknya kadang menerapkan sharding sejak awal “supaya siap scale nanti”, padahal volume data dan traffic aktual masih jauh dari batas kapasitas satu instance. Akibatnya, tim menanggung seluruh kompleksitas sharding — routing layer, distributed transaction, resharding strategy — tanpa manfaat nyata yang sepadan, sementara kecepatan development melambat karena setiap perubahan skema atau query harus mempertimbangkan implikasi lintas shard.
Tim under-estimate biaya operasional jangka panjang. Menerapkan sharding di hari pertama relatif “mudah” dibanding merawatnya bertahun-tahun kemudian. Masalah rebalancing, hot shard yang muncul seiring waktu, dan kebutuhan resharding yang tidak direncanakan sejak awal sering jadi kejutan yang menyakitkan. Tim yang tidak mengalokasikan kapasitas engineering berkelanjutan untuk hal ini akan mendapati sistemnya makin sulit dikelola seiring waktu, bukan makin stabil.
Alternatif yang lebih murah belum benar-benar dicoba habis. Banyak kasus di mana masalah performa sebenarnya bisa diselesaikan dengan index yang lebih baik, query yang dioptimasi, caching layer, read replica, atau partitioning saja — semuanya jauh lebih murah secara operasional dibanding sharding. Melompat langsung ke sharding tanpa mengevaluasi opsi-opsi ini secara serius adalah tanda keputusan arsitektur yang terburu-buru.
Pola akses data tidak mendukung co-location yang baik. Kalau mayoritas query aplikasi butuh join atau agregasi lintas entitas yang secara alami tidak bisa dikelompokkan dalam satu sharding key yang sama, sharding akan memaksa kamu melakukan scatter-gather query secara konstan — pada titik ini, kompleksitas yang ditanggung nyaris tidak memberi manfaat performa yang berarti dibanding tetap di satu instance yang dioptimasi dengan baik.
HINDARI sharding jika:
✗ volume data/traffic belum benar-benar mendekati batas kapasitas satu instance
✗ alternatif lebih murah (indexing, caching, read replica, partitioning) belum dicoba habis
✗ tim belum punya kapasitas untuk maintain routing layer dan strategi resharding jangka panjang
✗ pola query dominan butuh join/agregasi lintas entitas yang sulit di-co-locate
✗ keputusan diambil berdasarkan proyeksi pertumbuhan spekulatif, bukan data pertumbuhan aktual
Anti-Pattern Umum
# ✗ Sharding key dipilih tanpa mempertimbangkan pola akses,
# menyebabkan mayoritas query jadi scatter-gather lintas semua shard
sharding_key = id_internal_random
# ✓ Sharding key dipilih berdasarkan entitas yang paling sering
# diakses bersamaan (co-location), misal tenant_id untuk sistem multi-tenant
sharding_key = tenant_id
# ✗ Jumlah shard di-hardcode di banyak service tanpa rencana resharding,
# sehingga menambah shard baru berarti downtime besar dan migrasi manual masif
TOTAL_SHARDS = 4 // hardcoded di 15 service berbeda
# ✓ Gunakan consistent hashing dan shard map terpusat sejak awal,
# sehingga penambahan shard tidak memaksa migrasi data besar-besaran
shard = consistentHashRing.getShardFor(shardingKey)
# ✗ Auto-increment ID lokal per shard tanpa strategi keunikan global,
# menyebabkan konflik ID ketika data dari berbagai shard perlu digabungkan
id = localAutoIncrement()
# ✓ Gunakan Snowflake ID atau UUID v7 yang unik secara global
# dan tetap semi-berurutan untuk performa index yang baik
id = snowflakeIdGenerator.next()
Best Practice Sharding
1. Pilih Sharding Key Berdasarkan Pola Akses Paling Dominan
Sama seperti partition key, audit pola query paling sering dan paling berat sebelum menentukan sharding key. Prioritaskan sharding key yang memungkinkan co-location entitas yang sering diakses bersamaan, supaya join dan transaksi tetap bisa dilakukan secara lokal di dalam satu shard.
2. Desain untuk Resharding Sejak Hari Pertama
Jangan berasumsi jumlah shard akan selalu tetap. Gunakan consistent hashing atau directory-based sharding dengan shard map terpusat sejak awal, meski jumlah shard awal kamu hanya dua atau tiga — desain ini jauh lebih murah diterapkan sejak awal dibanding direfactor setelah sistem berjalan di production dengan data besar.
3. Gunakan ID Generator yang Sudah Terbukti
Hindari membangun skema ID unik global dari nol. Snowflake ID, UUID v7, atau layanan seperti Twitter Snowflake yang open-source sudah teruji menangani masalah ini dengan baik — memanfaatkannya menghindarkan kamu dari bug subtle terkait race condition ID generation yang sulit di-debug.
4. Rancang Strategi Cross-shard Query Sejak Awal, Bukan Reaktif
Identifikasi query mana yang secara alami akan butuh scatter-gather sejak fase desain, lalu putuskan secara sadar apakah itu akan diatasi lewat denormalisasi, agregasi di application layer, atau dihindari sepenuhnya lewat perubahan pola akses. Jangan menunggu sampai query semacam ini muncul di production sebagai masalah performa mendadak.
5. Monitoring per-Shard, Bukan Hanya Agregat
Pantau metrik beban (CPU, disk, query latency, write throughput) di level masing-masing shard secara individual, bukan hanya rata-rata gabungan seluruh sistem. Hot shard sering tersembunyi di balik metrik agregat yang terlihat sehat karena shard lain yang lebih ringan menutupi masalah di satu shard yang sudah kelebihan beban.
6. Uji Skenario Kegagalan Distributed Transaction Secara Rutin
Simulasikan skenario kegagalan di tengah proses commit lintas shard — koordinator crash, network partition antar shard, timeout — sebagai bagian dari testing rutin, bukan hanya berharap skenario itu tidak pernah terjadi di production. Sistem yang bergantung pada saga pattern khususnya perlu diuji ketahanannya terhadap kegagalan di tengah rangkaian langkah kompensasi.
Checklist singkat sebagai referensi cepat:
SEBELUM MEMUTUSKAN SHARDING:
□ Vertical scaling, read replica, dan partitioning sudah dievaluasi dan tidak mencukupi
□ Keputusan berdasarkan data pertumbuhan aktual, bukan proyeksi spekulatif
□ Sharding key dipilih berdasarkan audit pola akses nyata
□ Tim punya kapasitas berkelanjutan untuk maintain infrastruktur multi-shard
DESAIN ARSITEKTUR:
□ Strategi routing (client-side atau proxy) sudah ditentukan dengan trade-off yang dipahami
□ Consistent hashing atau shard map terpusat dipakai sejak awal
□ Strategi ID generation global (Snowflake/UUID v7) sudah dipilih
□ Strategi cross-shard query (co-location/denormalisasi/scatter-gather) sudah dirancang
OPERASIONAL BERKELANJUTAN:
□ Monitoring per-shard individual, bukan hanya agregat
□ Strategi resharding dan rebalancing terdokumentasi dan pernah diuji
□ Skenario kegagalan distributed transaction sudah diuji secara rutin
□ On-call/tim siap menangani insiden yang melibatkan koordinasi lintas shard
Ringkasan
- Sharding membagi data ke banyak instance database independen, berbeda dari partitioning yang tetap berada dalam satu instance — akibatnya, kemudahan transaksi dan join yang gratis di satu instance harus dibangun ulang secara eksplisit di layer aplikasi.
- Sharding relevan ketika write throughput sudah melampaui kapasitas satu instance, sesuatu yang tidak bisa diselesaikan read replica.
- Sharding key menentukan hampir semua trade-off berikutnya — pilih berdasarkan pola akses nyata, prioritaskan co-location entitas yang sering diakses bersamaan.
- Cross-shard join tidak native tersedia; strategi umum adalah denormalisasi, scatter-gather query, atau co-location by design.
- Distributed transaction lintas shard butuh trade-off eksplisit antara consistency dan availability — via two-phase commit (konsisten tapi rawan blocking) atau saga pattern (available tapi eventual consistency).
- Resharding yang aman butuh consistent hashing untuk meminimalkan data yang perlu dipindah, dibanding pendekatan modulo naif yang memindahkan hampir semua data sekaligus.
- ID unik global butuh strategi eksplisit seperti Snowflake ID atau UUID v7, karena auto-increment lokal per shard akan bentrok.
- Hindari sharding kalau volume belum benar-benar mendekati batas kapasitas satu instance, alternatif lebih murah belum dicoba habis, atau tim belum siap menanggung beban operasional jangka panjangnya.