Scalability #
Hampir semua sistem yang mengalami masalah skalabilitas mengalaminya dengan cara yang sama: di awal semuanya terasa baik. Database melayani ratusan user dengan lancar, response time di bawah 100ms, tidak ada yang perlu dikhawatirkan. Lalu trafik mulai naik. Query yang tadinya selesai dalam 50ms kini butuh 500ms. Laporan yang sebelumnya selesai dalam detik kini memakan menit dan menghabiskan CPU. Connection pool mulai penuh. Dan di suatu titik, sistem mulai timeout di sana-sini tanpa alasan yang jelas.
Inilah realita scaling database: ia tidak terasa sampai tiba-tiba terasa sangat nyata. Dan ketika sudah terasa, solusinya hampir selalu lebih mahal dan lebih sulit dari seharusnya — karena keputusan desain awal yang tidak mempertimbangkan pertumbuhan.
Scalability database bukan tentang memilih teknologi paling canggih sejak awal. Ia tentang memahami bottleneck yang akan datang, mengoptimalkan lapisan termurah terlebih dahulu, dan naik ke lapisan berikutnya hanya ketika benar-benar diperlukan. Artikel ini membahas seluruh tangga itu — dari optimasi query sampai sharding — beserta kapan dan mengapa berpindah dari satu level ke level berikutnya.
Scalability vs Performance: Perbedaan yang Sering Dibalik #
Sebelum membahas teknik, perlu dipahami dulu perbedaan mendasar antara dua konsep yang sering digunakan secara bergantian padahal maknanya berbeda.
Performance adalah seberapa cepat sistem melayani satu request pada kondisi saat ini. Sebuah query yang selesai dalam 10ms adalah query yang performant.
Scalability adalah seberapa baik sistem mempertahankan performanya ketika beban meningkat — lebih banyak user, lebih banyak data, lebih banyak query bersamaan. Sistem yang scalable tidak hanya cepat hari ini; ia tetap cepat ketika data tumbuh sepuluh kali lipat dan trafik naik dua puluh kali lipat.
Ilustrasi perbedaan performance vs scalability:
Sistem A — cepat tapi tidak scalable:
10 user → 30ms ✓
100 user → 150ms ✓
1.000 user → 2.5s ✗
10.000 user→ timeout ✗✗
Sistem B — scalable:
10 user → 50ms ✓ (sedikit lebih lambat dari A)
100 user → 60ms ✓
1.000 user → 80ms ✓
10.000 user→ 120ms ✓
Sistem A lebih cepat di awal.
Sistem B jauh lebih valuable di jangka panjang.
Database yang scalable tidak harus yang tercepat di kondisi ringan. Yang terpenting adalah ia tidak degradasi secara dramatis saat beban bertambah.
Database Selalu Menjadi Bottleneck Pertama #
Di arsitektur web modern, application server bersifat stateless — menambah instance baru semudah menambah container di Kubernetes. Tapi database tidak. Ia menyimpan state, dan state itu tidak bisa begitu saja diduplikasi tanpa koordinasi. Ini sebabnya database hampir selalu menjadi bottleneck lebih dulu dibanding application server.
Kenapa database selalu bottleneck lebih dulu:
Application server (stateless):
┌──────┐ ┌──────┐ ┌──────┐ ← tambah instance = scale linear
│ App │ │ App │ │ App │
└──┬───┘ └──┬───┘ └──┬───┘
│ │ │
└─────────┴─────────┘
│
▼
┌──────────────────────────┐
│ Database │ ← satu titik yang menerima semua beban
│ (stateful, tidak bisa │ dari semua instance
│ scale semudah app) │
└──────────────────────────┘
Setiap kali kamu menambah instance app → database semakin terbebani.
Menambah app instance tanpa mempersiapkan database = memperburuk bottleneck.
Ini juga mengapa solusi “tambah server” sering tidak membantu jika bottlenecknya ada di database. Yang bertambah hanya tekanan pada database, bukan kapasitasnya.
Bottleneck Ladder: Urutan yang Benar #
Kesalahan yang paling sering terjadi dalam scaling database adalah melompat langsung ke solusi yang kompleks — sharding, distributed database, CQRS — sebelum mengoptimalkan lapisan yang lebih sederhana dan lebih murah. Sharding yang dilakukan sebelum waktunya adalah salah satu keputusan teknis yang paling mahal untuk di-rollback.
Pendekatan yang benar adalah naik tangga satu anak tangga pada satu waktu, dan hanya berpindah ke anak tangga berikutnya ketika anak tangga sebelumnya sudah benar-benar dioptimalkan.
Bottleneck Ladder — urutan optimasi dari paling murah ke paling kompleks:
Anak tangga 6: Sharding
↑ hanya jika anak tangga 5 tidak cukup
Anak tangga 5: Arsitektur multi-database (CQRS, polyglot persistence)
↑ hanya jika anak tangga 4 tidak cukup
Anak tangga 4: Read replica + connection pooler (horizontal read scaling)
↑ hanya jika anak tangga 3 tidak cukup
Anak tangga 3: Caching layer (Redis/Memcached) + async processing
↑ hanya jika anak tangga 2 tidak cukup
Anak tangga 2: Optimasi query + index + partitioning
↑ hanya jika anak tangga 1 tidak cukup
Anak tangga 1: Vertical scaling (upgrade hardware)
↑ titik awal — paling murah, paling cepat diimplementasi
Prinsip: jangan naik ke anak tangga berikutnya sebelum
anak tangga yang ada benar-benar dioptimalkan.
Anak Tangga 1: Vertical Scaling #
Vertical scaling adalah menambah resource pada satu server database yang ada — lebih banyak CPU, lebih banyak RAM, storage yang lebih cepat. Ini adalah langkah pertama yang paling mudah dan paling cepat diimplementasi.
Vertical scaling:
Sebelum: Sesudah:
┌────────────────┐ ┌────────────────────────┐
│ DB Server │ │ DB Server (upgraded) │
│ 4 CPU │ →→→ │ 32 CPU │
│ 16 GB RAM │ │ 128 GB RAM │
│ HDD │ │ NVMe SSD │
└────────────────┘ └────────────────────────┘
Keuntungan:
→ Tidak ada perubahan arsitektur atau kode
→ Bisa dilakukan dalam hitungan jam (cloud: resize instance)
→ Efektif untuk bottleneck yang disebabkan resource murni
Batas:
→ Ada ceiling hardware — tidak bisa terus di-upgrade selamanya
→ Harga naik eksponensial seiring ukuran instance
→ Tidak menyelesaikan masalah arsitektural (query buruk tetap buruk)
→ Single point of failure tetap ada
Vertical scaling paling efektif sebagai langkah pertama dan sebagai “pembelian waktu” — ia memberi ruang untuk mengoptimalkan level berikutnya tanpa tekanan insiden. Tapi jangan jadikan ini strategi permanen.
Anak Tangga 2: Optimasi Query, Index, dan Partitioning #
Sebelum menambahkan infrastruktur baru, pastikan database yang ada sudah digunakan dengan benar. Sangat sering, masalah performa yang terasa seperti masalah skalabilitas ternyata adalah masalah query yang buruk — query tanpa index, N+1, SELECT * di tabel besar, atau OFFSET yang sangat besar.
Dampak optimasi query — sebelum dan sesudah:
Query tanpa index pada tabel 50 juta baris:
SELECT * FROM orders WHERE status = 'pending' AND created_at > '2025-01-01'
→ Full table scan: 4.2 detik, 50 juta baris dibaca
Query dengan index yang tepat:
CREATE INDEX idx_orders_status_created ON orders(status, created_at);
SELECT * FROM orders WHERE status = 'pending' AND created_at > '2025-01-01'
→ Index seek: 8ms, 12.000 baris dibaca
Optimasi query satu ini setara dengan meng-upgrade database ke server 10x lebih kencang
— tapi gratis dan tidak perlu downtime.
Tiga area yang paling sering diabaikan dan paling berdampak:
Area optimasi dengan dampak terbesar:
1. Query N+1 — membunuh performa secara diam-diam
✗ 1 query untuk list + N query untuk setiap item = 101 query untuk 100 item
✓ JOIN atau batch loading = 1-2 query untuk 100 item
2. SELECT * pada tabel lebar
✗ SELECT * FROM users — mengambil 40 kolom padahal hanya butuh 3
✓ SELECT id, name, email FROM users — data transfer 90% lebih kecil
3. OFFSET besar untuk pagination
✗ LIMIT 20 OFFSET 10000000 — database scan 10 juta baris lalu buang semuanya
✓ Keyset pagination: WHERE id > last_seen_id LIMIT 20 — O(log n)
Partitioning (dibahas di artikel sebelumnya) juga termasuk di anak tangga ini — ia adalah optimasi di level tabel yang bisa sangat efektif sebelum harus naik ke replikasi atau sharding.
Anak Tangga 3: Caching dan Async Processing #
Cara paling efektif mengurangi beban database adalah tidak menghit database sama sekali untuk data yang bisa di-cache. Caching layer seperti Redis atau Memcached bisa melayani jutaan request per detik dengan latency di bawah 1ms — jauh lebih cepat dari query database tercepat sekalipun.
Arsitektur dengan caching layer:
Request ─→ Application ─→ Cache (Redis)
│
┌─────▼──────┐
│ Cache hit? │
└─────┬──────┘
│
┌────────────┴────────────┐
│ YES │ NO
▼ ▼
Return dari cache Query Database
(< 1ms) → Simpan ke cache
→ Return result
(5–50ms)
Efek:
→ 80-95% request dilayani dari cache tanpa menyentuh database
→ Database hanya menerima 5-20% dari total request
→ Throughput efektif naik 5–20x tanpa mengubah database
Strategi caching yang tepat bergantung pada karakteristik data:
Memilih strategi caching:
Cache-aside (lazy loading):
→ App cek cache dulu, jika miss baru query DB, simpan hasilnya
→ Cocok untuk: data yang dibaca lebih sering dari ditulis
→ Trade-off: cache miss pertama tetap lambat
Write-through:
→ Setiap write ke DB juga langsung update cache
→ Cocok untuk: data yang harus selalu konsisten antara DB dan cache
→ Trade-off: setiap write lebih lambat (2 operasi)
Write-behind (write-back):
→ Write ke cache dulu, async flush ke DB belakangan
→ Cocok untuk: counter, view count, non-critical data
→ Trade-off: risiko data loss jika cache down sebelum flush ke DB
TTL-based expiry:
→ Data di cache otomatis expired setelah durasi tertentu
→ Cocok untuk: data yang boleh stale untuk beberapa detik/menit
→ Paling mudah diimplementasi, paling umum digunakan
Async processing juga masuk di level ini: operasi yang tidak perlu diproses synchronous — pengiriman email, pembuatan laporan, kalkulasi statistik — dipindahkan ke queue dan diproses oleh background worker. Database tidak dibebani oleh operasi yang tidak time-sensitive.
Anak Tangga 4: Read Replica #
Mayoritas sistem web bersifat read-heavy: rasio read vs write sering 80:20 atau bahkan 95:5. Read replica memungkinkan distribusi beban read ke server tambahan sementara semua write tetap ke primary.
Arsitektur read replica:
┌─────────────────────────────────────────────────────┐
│ Aplikasi │
└──────────────┬──────────────────────┬───────────────┘
│ Write │ Read
▼ ▼
┌──────────────────────┐ ┌───────────────────────────┐
│ Primary Database │ │ Load Balancer │
│ (semua write) │ │ (round-robin / least │
└──────────┬───────────┘ │ connection) │
│ replikasi └─────────┬──────────────────┘
│ │
┌────▼────────┐ ┌─────────▼────┐ ┌──────────────┐
│ Replica 1 │ │ Replica 2 │ │ Replica 3 │
│ (API read) │ │ (reporting) │ │ (backup) │
└─────────────┘ └──────────────┘ └──────────────┘
Kapasitas read: 3x lipat tanpa mengubah primary sama sekali
Kapasitas write: sama (primary tidak berubah)
Kompleksitas: sedang — perlu handle read-after-write, replication lag
Read replica adalah langkah horizontal scaling yang paling tidak invasif — tidak perlu mengubah schema, tidak perlu aplikasi yang rumit, dan bisa ditambah kapan saja. Ini biasanya langkah horizontal pertama yang diambil setelah optimasi query sudah maksimal.
Anak Tangga 5: Pemisahan Beban OLTP dan OLAP #
Sistem produksi sering memiliki dua jenis workload yang sangat berbeda karakteristiknya: OLTP (Online Transaction Processing) dan OLAP (Online Analytical Processing). Menjalankan keduanya di database yang sama adalah salah satu penyebab paling umum performa yang tidak bisa dijelaskan.
Perbedaan karakteristik OLTP dan OLAP:
┌─────────────────────┬────────────────────────┬──────────────────────────┐
│ Aspek │ OLTP │ OLAP │
├─────────────────────┼────────────────────────┼──────────────────────────┤
│ Tujuan │ Transaksi operasional │ Analisis dan reporting │
│ Query │ Sederhana, cepat │ Kompleks, lambat │
│ Data volume per query│ Sedikit baris │ Jutaan hingga miliaran │
│ Frekuensi │ Sangat tinggi (ribuan/s)│ Rendah (puluhan/hari) │
│ Latensi yang dibutuhkan│ < 100ms │ Detik hingga menit │
│ Contoh │ Login, checkout, bayar │ Laporan bulanan, dashboard│
│ Index yang optimal │ B-Tree per kolom │ Columnar, bitmap │
│ Storage optimal │ Row-based │ Column-based │
└─────────────────────┴────────────────────────┴──────────────────────────┘
Masalah jika dicampur:
→ Query analitik yang berjalan 10 menit mengunci resource CPU dan IO
→ Query OLTP yang seharusnya selesai dalam 20ms ikut terdampak
→ Latency transaksi user naik secara tidak terduga
→ Sulit di-debug karena tidak ada error, hanya lambat
Solusi: pisahkan database untuk OLTP dan OLAP. Untuk sebagian sistem, cukup dengan mengarahkan query analitik ke read replica yang didedikasikan. Untuk sistem yang lebih besar, gunakan database analitik yang terpisah (ClickHouse, BigQuery, Redshift, atau Snowflake) yang dirancang khusus untuk workload ini.
Arsitektur pemisahan OLTP dan OLAP:
┌──────────────┐ ┌─────────────────────┐
│ Transaksi │ → Primary DB → │ ETL / CDC pipeline │ → Analytical DB
│ (OLTP) │ │ (Debezium, Airbyte) │ (ClickHouse,
└──────────────┘ └─────────────────────┘ BigQuery, dll)
↓
Query user yang butuh
response < 100ms
↓
Query report yang boleh
selesai dalam menit
Anak Tangga 6: CQRS dan Polyglot Persistence #
CQRS (Command Query Responsibility Segregation) adalah pola arsitektur yang memisahkan model untuk operasi tulis (Command) dan operasi baca (Query) secara fundamental — bukan hanya di level koneksi, tapi di level data model dan bahkan database yang digunakan.
Arsitektur CQRS:
Write side (Command): Read side (Query):
┌─────────────────┐ ┌─────────────────────────┐
│ Command Handler│ │ Query Handler │
│ (create order, │ │ (get order detail, │
│ update status)│ │ list orders, search) │
└────────┬────────┘ └─────────────┬───────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────────────┐
│ Write Database │ │ Read Database │
│ (PostgreSQL, │ →event→ │ (bisa berbeda: │
│ normalized) │ │ Elasticsearch, Redis, │
└─────────────────┘ │ MongoDB, atau view │
│ yang didenormalisasi) │
└─────────────────────────┘
Keuntungan:
→ Read model bisa dioptimalkan untuk query tanpa menganggu write model
→ Bisa menggunakan database berbeda yang paling cocok untuk masing-masing use case
→ Read model bisa direbuild dari scratch jika perlu tanpa downtime
Kekurangan:
→ Eventual consistency antara write dan read model
→ Kompleksitas operasional yang jauh lebih tinggi
→ Debugging lebih sulit — data mungkin berbeda di write dan read store
CQRS baru masuk akal ketika read dan write memiliki kebutuhan yang sangat berbeda dan tidak bisa dilayani dengan baik oleh satu database. Jangan gunakan CQRS sebagai langkah pertama — kompleksitasnya sangat tinggi.
Mengenali Kapan Harus Naik ke Level Berikutnya #
Salah satu keputusan tersulit dalam scaling adalah menentukan kapan sudah waktunya naik ke level berikutnya. Naik terlalu cepat adalah over-engineering. Naik terlalu lambat adalah insiden.
Sinyal bahwa kamu perlu naik ke level berikutnya:
Dari vertical ke optimasi query:
→ CPU dan RAM sudah di-upgrade tapi latency tetap tinggi
→ Slow query log dipenuhi query yang bisa dioptimalkan
→ EXPLAIN menunjukkan full table scan pada tabel besar
Dari optimasi query ke caching:
→ Query sudah optimal tapi trafik terlalu tinggi untuk dilayani DB saja
→ Data yang sama dibaca berulang-ulang (produk populer, konfigurasi)
→ DB CPU tinggi karena volume query, bukan karena query itu sendiri berat
Dari caching ke read replica:
→ DB masih kewalahan meski cache sudah ada
→ Read traffic masih mendominasi (> 80% dari total query)
→ Trafik analitik mengganggu query operasional
Dari read replica ke pemisahan OLTP/OLAP:
→ Query analitik membebani replica yang harusnya untuk API
→ Kebutuhan data warehouse mulai terasa (historical analysis, BI tools)
Dari pemisahan OLTP/OLAP ke CQRS atau sharding:
→ Write rate sudah terlalu tinggi untuk satu primary
→ Data volume sudah melebihi kapasitas satu server (sharding)
→ Read dan write membutuhkan data model yang sangat berbeda (CQRS)
Observability: Fondasi dari Semua Keputusan Scaling #
Tidak ada keputusan scaling yang bisa dibuat dengan benar tanpa data. Tanpa observability, scaling adalah tebakan — dan tebakan yang salah dalam konteks ini bisa sangat mahal.
Metrik yang wajib ada sebelum membuat keputusan scaling:
Database level:
→ Query latency (P50, P95, P99) — bukan hanya rata-rata
→ Queries per second (QPS) — dibagi per jenis (SELECT, INSERT, UPDATE, DELETE)
→ Slow query count dan slow query log
→ Threads_connected vs max_connections
→ Buffer pool hit rate — harus di atas 95% untuk InnoDB
→ Replication lag (jika ada replica)
Tabel dan index level:
→ Tabel terbesar dan pertumbuhannya
→ Index yang tidak pernah digunakan
→ Query dengan rows_examined tertinggi (EXPLAIN)
Sistem level:
→ CPU utilization (user vs system vs iowait)
→ Disk I/O: read/write throughput, IOPS, latency
→ Memory: RAM usage, swap usage
→ Network: throughput antara app dan DB
Alert yang harus ada:
→ Query latency P99 > threshold (sesuaikan dengan SLA)
→ Slow query count melebihi N per menit
→ Connections mendekati max_connections
→ Replication lag > X detik
→ Disk usage > 80%
→ Buffer pool hit rate < 90%
-- Query yang berguna untuk observability tanpa tools tambahan
-- Top query berdasarkan total waktu eksekusi (MySQL Performance Schema)
SELECT
digest_text AS query_pattern,
count_star AS execution_count,
ROUND(avg_timer_wait / 1000000000, 3) AS avg_seconds,
ROUND(sum_timer_wait / 1000000000, 3) AS total_seconds,
ROUND(sum_rows_examined / count_star) AS avg_rows_examined
FROM performance_schema.events_statements_summary_by_digest
WHERE digest_text NOT LIKE '%performance_schema%'
ORDER BY sum_timer_wait DESC
LIMIT 20;
-- Cek buffer pool hit rate (harus > 95%)
SELECT
ROUND(
(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Innodb_buffer_pool_read_requests') /
((SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Innodb_buffer_pool_read_requests') +
(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Innodb_buffer_pool_reads')) * 100,
2) AS buffer_pool_hit_rate_pct;
-- Jika < 95%: pertimbangkan menambah innodb_buffer_pool_size
-- Jika buffer pool terlalu kecil, banyak data dibaca dari disk → lambat
-- Tabel terbesar di database
SELECT
table_name,
ROUND(data_length / 1024 / 1024, 1) AS data_mb,
ROUND(index_length / 1024 / 1024, 1) AS index_mb,
table_rows AS estimated_rows
FROM information_schema.tables
WHERE table_schema = DATABASE()
ORDER BY data_length + index_length DESC
LIMIT 15;
Anti-Pattern yang Harus Dihindari #
✗ Anti-pattern 1: upgrade hardware untuk menutupi query yang buruk
Server 64 CPU dan 512GB RAM tetap bisa dihancurkan oleh satu query
full table scan di tabel 1 miliar baris.
✓ Optimasi query dan index sebelum menyentuh hardware.
────────────────────────────────────────────────────────────────────────────────
✗ Anti-pattern 2: menjalankan query analitik di database transaksi
Laporan bulanan yang full-scan jutaan baris selama 10 menit
membuat semua endpoint user mengalami latency naik.
✓ Pisahkan analytic workload ke replica khusus atau database analitik.
────────────────────────────────────────────────────────────────────────────────
✗ Anti-pattern 3: menambah read replica tanpa menangani replication lag
Membaca dari replica langsung setelah write → data lama.
User melihat perubahan yang baru saja mereka buat tidak tampil.
✓ Handle read-after-write: baca dari primary untuk operasi kritis,
atau tunggu replica catch-up sebelum redirect ke replica.
────────────────────────────────────────────────────────────────────────────────
✗ Anti-pattern 4: sharding terlalu dini
Sharding sebelum waktunya menambah kompleksitas operasional yang sangat tinggi:
cross-shard query, distributed transaction, resharding yang mahal.
✓ Exhausted semua level yang lebih sederhana sebelum sharding.
Sebagian besar sistem tidak perlu sharding sampai ratusan juta pengguna.
────────────────────────────────────────────────────────────────────────────────
✗ Anti-pattern 5: scaling tanpa observability
Menambah replica, menambah cache, mengubah konfigurasi — semua tanpa
mengukur apakah perubahan itu benar-benar membantu.
✓ Ukur baseline sebelum perubahan. Ukur lagi setelah perubahan.
Keputusan scaling harus didorong data, bukan asumsi.
────────────────────────────────────────────────────────────────────────────────
✗ Anti-pattern 6: over-engineering dari awal
Membangun sistem dengan CQRS, sharding, dan multi-database
untuk aplikasi yang baru punya 1.000 user.
✓ Start simple. Scale when needed.
Kompleksitas prematur adalah pembunuh produktivitas tim.
Checklist Review Scalability #
FONDASI (harus ada sebelum memikirkan scaling):
□ Slow query log aktif dan dipantau secara rutin
□ EXPLAIN dijalankan untuk semua query yang penting dan sering dijalankan
□ Index sudah optimal — tidak ada full table scan pada tabel besar
□ N+1 query sudah dieliminasi
□ Pagination menggunakan keyset (cursor-based), bukan OFFSET besar
□ SELECT * sudah diganti dengan kolom yang diperlukan saja
CACHING:
□ Data yang sering dibaca dan jarang berubah sudah di-cache
□ Cache invalidation strategy sudah terdefinisi dengan jelas
□ Cache hit rate dimonitor (target > 80% untuk data yang sesuai)
READ SCALING:
□ Read replica sudah dipisah dari primary untuk query berat
□ Query analitik dan reporting tidak berjalan di primary
□ Replication lag dimonitor dengan alert yang jelas
□ Read-after-write sudah ditangani di application layer
OLTP VS OLAP:
□ Query analitik berat sudah diarahkan ke replica khusus atau DB terpisah
□ Tidak ada query yang berjalan lebih dari 30 detik di primary produksi
OBSERVABILITY:
□ Metrik P50, P95, P99 query latency sudah tersedia
□ Buffer pool hit rate dimonitor (target > 95% untuk InnoDB)
□ Disk I/O, CPU, dan memory database dimonitor
□ Alert sudah dipasang untuk semua metrik kritis
□ Baseline performa terdokumentasi sebelum setiap perubahan besar
SCALING DECISION:
□ Keputusan untuk naik ke level scaling berikutnya didasarkan pada data
□ Tidak ada sharding sebelum semua level yang lebih sederhana dioptimalkan
□ Kompleksitas setiap keputusan scaling dipahami dan didokumentasikan
Ringkasan #
- Scalability adalah kemampuan mempertahankan performa saat beban naik — bukan seberapa cepat sistem hari ini, tapi seberapa baik ia tetap cepat ketika data dan user bertambah sepuluh kali lipat.
- Database selalu menjadi bottleneck lebih dulu — karena stateful dan tidak bisa di-scale semudah application server stateless. Menambah app instance tanpa mempersiapkan database memperburuk bottleneck.
- Naik tangga satu anak pada satu waktu — vertical scaling → optimasi query → caching → read replica → pemisahan OLTP/OLAP → CQRS/sharding. Jangan loncat langkah.
- Optimasi query adalah anak tangga yang paling sering dilewatkan — satu index yang tepat bisa membuat query 100x lebih cepat tanpa perubahan infrastruktur apapun.
- Caching adalah cara termudah mengurangi beban database — 80-95% request bisa dilayani dari cache, membuat database hanya perlu melayani 5-20% dari total traffic.
- Read replica untuk read scaling, bukan solusi write scaling — menambah replica meningkatkan kapasitas read, tapi write tetap menjadi bottleneck di primary.
- OLTP dan OLAP tidak boleh dicampur — query analitik yang berjalan menit bisa membuat semua query operasional user ikut terdampak. Pisahkan ke replica khusus atau database analitik.
- Sharding adalah langkah terakhir, bukan langkah pertama — ia menyelesaikan masalah yang tidak bisa diselesaikan cara lain, tapi dengan biaya kompleksitas yang sangat tinggi. Sebagian besar sistem tidak perlu sharding.
- Observability adalah fondasi semua keputusan scaling — tanpa P95/P99 latency, buffer pool hit rate, slow query log, dan disk I/O, semua keputusan scaling adalah tebakan.
- Over-engineering lebih berbahaya dari under-scaling — membangun arsitektur distributed yang kompleks sebelum waktunya membunuh produktivitas tim tanpa manfaat nyata. Start simple, scale when needed, scale with evidence.