Kenapa ini penting
Coding interview menguji apakah kamu bisa menyelesaikan masalah kecil yang terdefinisi jelas. System design interview menguji hal yang berbeda: bisakah kamu merancang sistem nyata dari permintaan yang sengaja dibuat kabur, seperti "Design a URL shortener" atau "Design a news feed". Tech Interview Handbook menyebut tujuannya adalah menilai kemampuan merancang sistem software dunia nyata, dan bahwa ronde ini biasanya diberikan kepada kandidat yang lebih senior. Namun halaman persiapan SDE II Amazon menyebut minimal satu soal system design, dan untuk posisi mid-level di banyak perusahaan versi ringannya tetap muncul. Untuk posisi front-end, handbook yang sama menyebut format front-end system design: merancang arsitektur aplikasi client atau komponen UI kompleks, dengan penekanan pada apa yang terjadi di client dan desain API antara client dan server.
Tidak ada satu jawaban benar. Yang dinilai: apakah kamu bertanya sebelum merancang, apakah kamu bisa memperkirakan skala, apakah kamu tahu komponen standar (cache, CDN, load balancer, replica, queue) dan kapan memakainya, dan apakah kamu bisa menjelaskan trade-off. Analogi: merancang sistem itu seperti merancang dapur restoran. Restoran 10 meja dan 1.000 meja sama-sama butuh kompor, tapi yang besar butuh banyak juru masak (server), pelayan pembagi pesanan (load balancer), bahan setengah jadi yang disiapkan di depan (cache), dan antrean tiket pesanan (queue).
Konsep inti
Kerangka menjawab (45-60 menit)
| Langkah | Porsi waktu kira-kira | Yang dilakukan |
|---|---|---|
| 1. Requirements | 5-10 menit | Functional (fitur apa) dan non-functional (skala, latensi, ketersediaan, konsistensi). Sepakati yang tidak dikerjakan. |
| 2. Estimasi | 3-5 menit | QPS baca/tulis, storage, bandwidth. Angka kasar cukup. |
| 3. API | 5 menit | Endpoint, request/response, pagination, autentikasi. |
| 4. Data model | 5 menit | Tabel/koleksi, kunci, indeks, pilihan SQL vs NoSQL. |
| 5. High-level design | 10-15 menit | Gambar kotak: client, LB, app server, cache, DB, queue, worker, CDN. |
| 6. Deep dive & scaling | 10-15 menit | Bottleneck, kegagalan, trade-off; ikuti arah yang diminati interviewer. |
Porsi waktu ini adalah pola umum, bukan aturan resmi. Yang penting: jangan langsung menggambar kotak. Lima menit pertama untuk pertanyaan seperti "How many daily active users?", "Is the read-to-write ratio heavily skewed toward reads?", "Do we need analytics?", "Can links expire?"
Komponen standar dan kapan dipakai
| Komponen | Masalah yang diselesaikan | Trade-off |
|---|---|---|
| Load balancer | Membagi trafik ke banyak server, menghapus server yang mati dari rotasi | Tambahan hop; perlu health check |
| Cache (Redis, in-memory) | Baca berulang yang mahal; latensi rendah | Data bisa basi; strategi invalidasi; memori terbatas (eviction LRU) |
| CDN | Aset statis dan konten yang bisa di-cache dekat dengan user | Invalidasi cache; tidak cocok untuk data personal |
| Read replica | Banyak baca, sedikit tulis | Replication lag: baca dari replica bisa tertinggal |
| Sharding / partitioning | Data atau tulis terlalu besar untuk satu mesin | Query lintas shard sulit; pemilihan shard key krusial; resharding mahal |
| Message queue (SQS, Kafka, RabbitMQ) | Pekerjaan lambat atau lonjakan trafik; memisahkan producer dan consumer | Eventual consistency; perlu retry dan idempotency |
| Rate limiter | Mencegah penyalahgunaan dan melindungi backend | Bisa menolak user sah saat lonjakan |
Konsep horizontal scaling (tambah mesin) vs vertical scaling (mesin lebih besar), dan stateless app server (sesi disimpan di luar server, misalnya di cache atau token) supaya load balancer bebas mengarahkan request ke server mana pun.
Contoh: URL shortener
- Requirements: buat link pendek dari URL panjang; redirect saat link dibuka; opsional: custom alias, kedaluwarsa, hitungan klik. Non-functional: redirect harus sangat cepat, sistem sangat dominan baca, kode tidak boleh bisa ditebak berurutan (opsional, diskusikan).
- Estimasi (asumsi latihan, lihat kode): 1 juta URL baru/hari, rasio baca:tulis 100:1.
- API:
POST /api/urls { longUrl, alias? } → { code, shortUrl };GET /:code → 301/302 redirect. Diskusikan 301 (di-cache browser, hitungan klik hilang) vs 302 (setiap klik lewat server, bisa dihitung). - Data model:
urls(code PK, long_url, created_at, expires_at, user_id). Akses dominan berdasarkancode, jadi key-value store atau tabel SQL dengan primary key sama-sama masuk akal. - High-level: client → LB → app server stateless → cache (code → longUrl) → DB. Pembuatan kode: counter/sequence global yang di-encode base62, atau hash dari URL dengan penanganan tabrakan.
- Scaling: cache untuk link populer, read replica, sharding berdasarkan
code, analitik klik dikirim ke queue supaya redirect tidak ikut lambat.
Front-end system design
Untuk role front-end, soalnya bisa "Design a news feed", "Design an autocomplete component", atau "Design an image carousel" (tiga contoh yang dibahas di Front End System Design Playbook GreatFrontEnd). Kerangka yang bisa dipakai (pola umum):
| Aspek | News feed | Autocomplete |
|---|---|---|
| Requirements | Posting teks/gambar, like, infinite scroll | Saran saat mengetik, keyboard, mobile |
| Arsitektur komponen | Feed, Post, Composer, store data |
Input, ResultsList, cache, controller fetch |
| Data & API | Cursor pagination (?cursor=...&limit=20), bukan offset, karena item baru menggeser offset |
GET /suggest?q= dengan debounce, batalkan request lama |
| State | Normalisasi (post by id), optimistic update untuk like | Cache per query (LRU), status loading/error |
| Performa | Virtualized list, lazy load gambar, srcset |
Debounce, cache, batas jumlah hasil |
| Aksesibilitas | Landmark, fokus setelah load more, alt text | Pola combobox APG, aria-activedescendant |
| Edge cases | Offline, post gagal kirim, duplikat saat pagination | Race condition, query kosong, karakter khusus |
Contoh kode
Kode ini menghitung estimasi URL shortener, mengubah ID menjadi kode base62, mendemokan cache LRU, dan pemilihan shard sederhana. Semua angka trafik adalah asumsi latihan, bukan data perusahaan mana pun.
// Estimasi kasar (back-of-the-envelope) URL shortener, angka asumsi latihan, bukan data nyata
const writesPerDay = 1_000_000; // asumsi: 1 juta URL baru per hari
const readWriteRatio = 100; // asumsi: 100 klik per URL dibuat
const secondsPerDay = 86_400;
const writeQps = writesPerDay / secondsPerDay;
const readQps = writeQps * readWriteRatio;
const bytesPerRow = 500; // asumsi: URL panjang + metadata
const storage5y = writesPerDay * 365 * 5 * bytesPerRow;
console.log('write QPS ~', writeQps.toFixed(1), '| read QPS ~', Math.round(readQps), '| peak (x3) ~', Math.round(readQps * 3));
// → write QPS ~ 11.6 | read QPS ~ 1157 | peak (x3) ~ 3472
console.log('total URL 5 tahun:', (writesPerDay * 365 * 5).toLocaleString('en-US'), '| storage ~', (storage5y / 1e9).toFixed(0), 'GB');
// → total URL 5 tahun: 1,825,000,000 | storage ~ 913 GB
console.log('kombinasi base62 7 karakter:', (62 ** 7).toLocaleString('en-US'));
// → kombinasi base62 7 karakter: 3,521,614,606,208
// Base62: ubah ID numerik (dari counter/DB sequence) jadi kode pendek
const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
function encode(num) {
if (num === 0) return ALPHABET[0];
let out = '';
while (num > 0) { out = ALPHABET[num % 62] + out; num = Math.floor(num / 62); }
return out;
}
function decode(str) { let n = 0; for (const ch of str) n = n * 62 + ALPHABET.indexOf(ch); return n; }
for (const id of [0, 61, 62, 125, 1_000_000, 3_500_000_000_000]) console.log(id, '->', encode(id), '->', decode(encode(id)));
// → 0 -> 0 -> 0
// → 61 -> Z -> 61
// → 62 -> 10 -> 62
// → 125 -> 21 -> 125
// → 1000000 -> 4c92 -> 1000000
// → 3500000000000 -> ZCpdjxe -> 3500000000000
// Cache LRU (Least Recently Used) memakai urutan penyisipan Map
class LRUCache {
constructor(capacity) { this.capacity = capacity; this.map = new Map(); }
get(key) {
if (!this.map.has(key)) return undefined;
const v = this.map.get(key); this.map.delete(key); this.map.set(key, v); // pindah ke "paling baru"
return v;
}
set(key, value) {
this.map.delete(key); this.map.set(key, value);
if (this.map.size > this.capacity) this.map.delete(this.map.keys().next().value); // buang paling lama
}
}
const cache = new LRUCache(2);
cache.set('abc', 'https://example.com/a'); cache.set('xyz', 'https://example.com/x');
cache.get('abc'); // abc jadi paling baru
cache.set('k9', 'https://example.com/k'); // xyz terbuang
console.log([...cache.map.keys()], cache.get('xyz'));
// → [ 'abc', 'k9' ] undefined
// Sharding sederhana: pilih shard dari hash kode
function shardFor(code, shards) { let h = 0; for (const ch of code) h = (h * 31 + ch.charCodeAt(0)) >>> 0; return h % shards; }
console.log(['abc', 'xyz', 'k9', '4c92'].map(c => `${c}->shard ${shardFor(c, 4)}`).join(', '));
// → abc->shard 2, xyz->shard 1, k9->shard 2, 4c92->shard 0
Dari estimasi ini kamu bisa berargumen: tulis hanya sekitar 12 per detik (satu database cukup untuk tulis), baca sekitar 1.000-3.500 per detik (cache sangat membantu), storage kurang dari 1 TB dalam 5 tahun (masih muat di satu database besar, sharding belum wajib di awal), dan 7 karakter base62 memberi sekitar 3,5 triliun kombinasi, jauh di atas 1,8 miliar URL. Kalimat seperti ini, "the numbers tell me we don't need sharding on day one", adalah yang membedakan jawaban matang dari jawaban yang asal menambahkan semua komponen.
Catatan shardFor: hash % N sederhana membuat hampir semua kunci pindah shard ketika N berubah. Sebutkan consistent hashing sebagai perbaikannya kalau interviewer bertanya tentang menambah shard.
Kesalahan umum
- Langsung menggambar arsitektur tanpa requirements. Cara benar: 5 menit pertama untuk bertanya dan menuliskan scope.
- Menambahkan Kafka, microservices, dan sharding untuk sistem kecil. Cara benar: biarkan estimasi menentukan kebutuhan; mulai sederhana, lalu scale.
- Mengabaikan kegagalan. "Apa yang terjadi kalau cache mati? Kalau DB primary mati?" Cara benar: sebutkan replica, failover, dan fallback ke DB.
- Cache tanpa strategi invalidasi. Cara benar: sebutkan TTL, cache-aside (baca cache, kalau miss ambil dari DB lalu simpan), dan apa yang terjadi saat data berubah.
- Front-end system design yang hanya membahas komponen visual. Cara benar: bahas juga API, state management, performa, aksesibilitas, dan edge case jaringan.
Latihan
- Mudah, Estimasi: aplikasi chat dengan 10 juta pesan per hari, rata-rata 200 byte per pesan. Berapa QPS tulis rata-rata dan storage per tahun?
- Sedang, Implementasikan rate limiter token bucket (kapasitas 3, isi ulang 1 token/detik) yang waktunya disuntikkan agar bisa dites.
- Menantang, Implementasikan cursor pagination untuk news feed yang diurutkan
createdAtterbaru, dengan tie-breakeridagar tidak ada duplikat atau item terlewat.
Lihat solusi
1. 10.000.000 / 86.400 ≈ 116 tulis per detik rata-rata (sebutkan peak bisa beberapa kali lipat). Storage: 10.000.000 × 200 byte × 365 ≈ 730 GB per tahun sebelum replikasi, indeks, dan lampiran.
console.log((10_000_000 / 86_400).toFixed(0), 'QPS |', (10_000_000 * 200 * 365 / 1e9).toFixed(0), 'GB/tahun');
// → 116 QPS | 730 GB/tahun
2.
class TokenBucket {
constructor(capacity, refillPerSec, now = 0) { this.capacity = capacity; this.tokens = capacity; this.rate = refillPerSec; this.last = now; }
allow(now) {
// isi ulang sesuai waktu yang berlalu, tidak melebihi kapasitas
this.tokens = Math.min(this.capacity, this.tokens + ((now - this.last) / 1000) * this.rate);
this.last = now;
if (this.tokens >= 1) { this.tokens -= 1; return true; }
return false;
}
}
const b = new TokenBucket(3, 1, 0);
console.log([0, 0, 0, 0, 500, 1000, 1000, 5000, 5000, 5000, 5000].map(t => b.allow(t) ? 'Y' : 'n').join(''));
// → YYYnnYnYYYn
Burst 3 request di t=0 lolos, yang ke-4 ditolak; di t=1000 ada 1 token baru; di t=5000 bucket penuh lagi (maksimal 3).
3.
function getFeedPage(posts, { cursor = null, limit = 2 } = {}) {
const sorted = posts.toSorted((a, b) => b.createdAt - a.createdAt || b.id - a.id);
// mulai dari item pertama yang "lebih tua" dari cursor (createdAt, id)
const start = cursor ? sorted.findIndex(p => p.createdAt < cursor.createdAt || (p.createdAt === cursor.createdAt && p.id < cursor.id)) : 0;
if (start === -1) return { items: [], nextCursor: null };
const items = sorted.slice(start, start + limit);
const last = items.at(-1);
return { items: items.map(p => p.id), nextCursor: start + limit < sorted.length ? { createdAt: last.createdAt, id: last.id } : null };
}
const posts = [{ id: 1, createdAt: 100 }, { id: 2, createdAt: 200 }, { id: 3, createdAt: 200 }, { id: 4, createdAt: 300 }, { id: 5, createdAt: 50 }];
let page = getFeedPage(posts); const pages = [page.items];
while (page.nextCursor) { page = getFeedPage(posts, { cursor: page.nextCursor }); pages.push(page.items); }
console.log(JSON.stringify(pages));
// → [[4,3],[2,1],[5]]
Post 2 dan 3 punya createdAt sama; tanpa tie-breaker id, salah satunya bisa terlewat atau muncul dua kali di halaman berbeda. Di database sungguhan, ini diterjemahkan menjadi WHERE (created_at, id) < ($1, $2) ORDER BY created_at DESC, id DESC LIMIT $3 dengan indeks gabungan (created_at, id) (sintaks row comparison seperti ini didukung PostgreSQL; cek dokumentasi database yang kamu pakai).
Ringkasan
- Kerangka: requirements → estimasi → API → data model → high-level design → deep dive & scaling.
- Estimasi kasar menentukan kebutuhan komponen; jangan menambahkan kompleksitas tanpa alasan angka.
- Komponen standar: load balancer, cache, CDN, read replica, sharding, message queue, rate limiter, masing-masing dengan trade-off.
- URL shortener: base62 dari ID, cache untuk baca dominan, 301 vs 302, analitik lewat queue.
- Front-end system design: arsitektur komponen, API (cursor pagination), state, performa, aksesibilitas, edge case.