kodingup.camp

Beranda/Siap Kerja US: Interview Teknis

System Design Dasar

Modul 10 dari 117 menit bacaSystem Design Basics

Isi modul

    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

    1. 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).
    2. Estimasi (asumsi latihan, lihat kode): 1 juta URL baru/hari, rasio baca:tulis 100:1.
    3. 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).
    4. Data model: urls(code PK, long_url, created_at, expires_at, user_id). Akses dominan berdasarkan code, jadi key-value store atau tabel SQL dengan primary key sama-sama masuk akal.
    5. 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.
    6. 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

    1. Langsung menggambar arsitektur tanpa requirements. Cara benar: 5 menit pertama untuk bertanya dan menuliskan scope.
    2. Menambahkan Kafka, microservices, dan sharding untuk sistem kecil. Cara benar: biarkan estimasi menentukan kebutuhan; mulai sederhana, lalu scale.
    3. Mengabaikan kegagalan. "Apa yang terjadi kalau cache mati? Kalau DB primary mati?" Cara benar: sebutkan replica, failover, dan fallback ke DB.
    4. 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.
    5. Front-end system design yang hanya membahas komponen visual. Cara benar: bahas juga API, state management, performa, aksesibilitas, dan edge case jaringan.

    Latihan

    1. 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?
    2. Sedang, Implementasikan rate limiter token bucket (kapasitas 3, isi ulang 1 token/detik) yang waktunya disuntikkan agar bisa dites.
    3. Menantang, Implementasikan cursor pagination untuk news feed yang diurutkan createdAt terbaru, dengan tie-breaker id agar 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

    Video pendamping

    Kalau penjelasan di atas belum masuk, tonton ini. Video dari kanal YouTube pihak lain.

    20 System Design Concepts Explained in 10 MinutesNeetCode · Bahasa InggrisBuka di YouTube

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.