Studi kasus: URL shortener
Base62 vs hash, sistem read-heavy, cache untuk link populer, dan redirect 301 vs 302, dikerjakan dengan kerangka 5 langkah
Sebelumnya, lesson 0027 memberi kerangka 5 langkah. Lesson ini menjalankannya penuh untuk soal yang paling sering muncul: "rancang layanan seperti bit.ly". Perhatikan urutannya: angka dulu, desain v1 sederhana, lalu setiap komponen tambahan dibenarkan angka.
Langkah 1 — Klarifikasi requirement
- Functional: user mengirim URL panjang dan mendapat URL pendek; membuka URL pendek mengarahkan (redirect) ke URL asli; link boleh punya tanggal kedaluwarsa. Di luar scope: custom alias, akun, dashboard.
- Skala (asumsi, disetujui interviewer): 1 juta URL baru per hari; rasio baca:tulis 100:1; data disimpan 5 tahun.
- Non-functional: redirect p99 < 100 ms; availability 99,9%; link baru boleh telat terlihat beberapa detik; kode pendek tidak boleh dobel.
Langkah 2 — Estimasi
| Besaran | Hitungan | Hasil |
|---|---|---|
| QPS tulis | 106 ÷ 105 detik | ±10 QPS, puncak ±30 |
| QPS baca | 100 × QPS tulis | ±1.000 QPS, puncak ±3.000 (read-heavy) |
| Jumlah URL 5 tahun | 106 × 365 × 5 | ±1,8 × 109 |
| Storage | 1,8 × 109 × ±500 byte (URL + kode + metadata) | ±1 TB |
| Panjang kode | 626 ≈ 5,7 × 1010; 627 ≈ 3,5 × 1012 | 7 karakter base62 memberi ruang ±2.000× jumlah URL |
| Cache link populer | 10 juta link terpanas × 500 byte | ±5 GB, muat di satu node cache |
Kesimpulan angka: tulis sangat kecil, baca 100× lebih besar, data ±1 TB. Ini sistem read-heavy, jadi deep dive nanti berfokus pada jalur baca, bukan pada menskalakan tulis.
Langkah 3 — API dan data model
POST /api/urls { "longUrl": "https://…", "expiresAt": "2027-01-01" }
→ 201 Created { "shortUrl": "https://sho.rt/aZ3kQ9x" }
GET /{kode}
→ 302 Found Location: https://… (URL asli)
→ 404 Not Found (kode tidak ada atau kedaluwarsa)
Tabel urls
kode varchar(7) PRIMARY KEY ← lookup redirect lewat index primary key
long_url text NOT NULL
created_at timestamptz
expires_at timestamptz NULL
Cara membuat kode. Dua pilihan yang perlu bisa Anda bandingkan:
| Counter + base62 | Hash URL dipotong 7 karakter | |
|---|---|---|
| Cara | Ambil angka unik dari sequence database, lalu ubah ke basis 62 (0-9a-zA-Z): 125 = 2×62 + 1 → "21" | Hitung hash (mis. SHA-256) dari URL, ambil 7 karakter base62 pertama |
| Collision | Tidak ada: setiap angka dipakai sekali | Pasti terjadi: dengan 1,8 × 109 kode di ruang 3,5 × 1012, perkiraan birthday n²/2N ≈ 4,6 × 105 pasangan bentrok |
| Kelemahan | Kode berurutan bisa ditebak; butuh satu sumber angka | Butuh cek dan ulangi (tambah salt) saat bentrok |
| Kelebihan | Sederhana dan pendek | URL yang sama menghasilkan kode yang sama (dedupe gratis) |
Untuk v1, counter cukup: PostgreSQL CREATE SEQUENCE membuat "a new sequence number generator", dan nilainya diambil dengan nextval. [1]
Puncak 30 QPS tulis jauh dari batas satu sequence, jadi ID generator terdistribusi (mis. jatah range angka per server) belum dibenarkan.
Kalau memilih hash, collision ditangkap oleh unique constraint, yang memastikan nilai kolom "is unique among all the rows in the table"; insert yang bentrok gagal, lalu ulangi dengan salt. [2]
Latihan 1 — Kode pendek
Jawab tanpa scroll ke atas. Soal yang sama akan muncul lagi, diacak, di review 0035.
Kuis di lesson ini adalah feedback cepat, bukan bukti penguasaan; yang dicatat hanya hasil review 0035.
Langkah 4 — High-level design (v1)
Desain v1 paling sederhana yang memenuhi angka langkah 2: satu app server stateless dan satu PostgreSQL.
Tulis: nextval → base62 → INSERT. Baca: SELECT long_url FROM urls WHERE kode = $1 lewat index primary key → 302.
Latihan 2 — Gambar diagram v1 sendiri dulu
Gambar diagram v1 Anda sendiri dulu di kertas: kotak apa saja, panah ke mana, dan jalur satu request tulis serta satu request baca. Setelah selesai, buka jawabannya.
Lihat jawaban
Jalur baca: client → GET /aZ3kQ9x → app server → lookup primary key → 302 + Location. Jalur tulis: POST → nextval → base62 → INSERT → 201.
Langkah 5 — Deep dive, bottleneck, trade-off
Evolusi dari v1 ke v2, satu komponen per alasan:
- Availability 99,9% (jatah ±43 menit/bulan): satu app server adalah single point of failure. Tambah server kedua di belakang load balancer; server stateless, jadi tidak ada session yang harus dipindah.
- 3.000 QPS baca di puncak: ukur dulu p99 query dan CPU database. Kalau p99 redirect mendekati 100 ms, tambah cache-aside di Redis untuk link populer (±5 GB).
Redis menyarankan
allkeys-lru"when you expect that a subset of elements will be accessed far more often than the rest", pola yang cocok dengan link viral. [3] Biayanya: satu komponen lagi, dan link yang dihapus atau kedaluwarsa bisa tetap terlayani sampai TTL cache habis. - Cache miss masih membebani database: baru tambah read replica. Link baru yang telat beberapa detik terlihat di replica sudah diizinkan requirement.
- Sharding belum perlu: ±1 TB dalam 5 tahun dan 30 QPS tulis masih muat di satu primary. Sebut sinyal kapan naik: storage atau tulis mendekati batas satu node; saat itu shard berdasarkan hash kode.
301 atau 302? Ini trade-off yang hampir selalu ditanyakan:
| 301 Moved Permanently | 302 Found | |
|---|---|---|
| Arti di RFC 9110 | Resource punya URI permanen baru; "any future references to this resource ought to use one of the enclosed URIs", dan 301 "is heuristically cacheable" [4] | Resource sementara berada di URI lain; "the client ought to continue to use the target URI for future requests" [4] |
| Efek | Browser bisa menyimpan redirect, klik berikutnya tidak sampai ke server: beban turun | Setiap klik sampai ke server: bisa dihitung, tujuan bisa diubah atau dikedaluwarsakan |
| Pilih kalau | Beban baca adalah masalah utama dan analytics tidak dibutuhkan | Butuh analytics klik atau link bisa berubah (pilihan v1 di sini) |
Latihan 3 — Redirect dan jalur baca
Latihan lisan — Ucapkan dengan suara keras
Pasang timer dan jawab dengan suara keras, seolah sedang interview. Jangan buka jawaban contoh sebelum selesai.
"Rancang URL shortener untuk 1 juta URL baru per hari. Mulai dari v1, lalu jelaskan evolusinya." (90 detik)
Bandingkan dengan jawaban contoh dan checklist-nya
"Dengan rasio baca:tulis 100:1, tulisnya sekitar 10 QPS dan baca 1.000 QPS, puncak sekitar 3.000; dalam 5 tahun ada 1,8 miliar URL, sekitar 1 TB. Tujuh karakter base62 memberi 3,5 triliun kombinasi, jadi cukup. Kodenya saya buat dari sequence database lalu di-encode base62, jadi tidak ada collision; kalau pakai hash, saya harus menangkap collision dengan unique constraint dan mengulang dengan salt. v1-nya satu app server dan satu PostgreSQL dengan kode sebagai primary key. Untuk availability 99,9% saya tambah server kedua di belakang load balancer. Setelah mengukur p99 baca di puncak, saya tambah cache-aside di Redis untuk link populer, sekitar 5 GB. Redirect saya pakai 302 supaya setiap klik terhitung; 301 mengurangi beban karena browser menyimpannya, tapi analytics hilang. Sharding belum perlu karena 1 TB dan 30 QPS tulis masih muat di satu node."
Checklist: apakah Anda menyebut…
- estimasi: ±10 QPS tulis, ±1.000 QPS baca (puncak ±3.000), ±1 TB dalam 5 tahun?
- kode base62 dari sequence, atau hash dengan penanganan collision, dan 7 karakter cukup?
- v1: app server dan satu database dengan kode sebagai primary key?
- 301 (cacheable, beban turun) vs 302 (setiap klik terhitung, link bisa diubah)?
- cache-aside untuk link populer setelah mengukur p99 atau beban database?
- membenarkan komponen dengan angka (LB untuk availability, cache untuk QPS baca, sharding belum perlu)?
Lulus jika minimal 4 dari 6.
Berikutnya, lesson 0029: rate limiter terdistribusi, dengan counter di Redis dan race condition-nya.
Istilah: GLOSSARY.md (bagian "Studi kasus") · Indeks: semua lesson System Design
Ada yang belum jelas? Tanyakan ke saya (agent) di terminal, misalnya: "bagaimana mencegah kode berurutan mudah ditebak?", "bagaimana menghapus link kedaluwarsa dari 1,8 miliar baris?", atau "kapan 308 lebih tepat dari 301?"
Sumber: [1] PostgreSQL docs: CREATE SEQUENCE · [2] PostgreSQL docs: Unique Constraints · [3] Redis docs: Key eviction · [4] RFC 9110: HTTP Semantics (301, 302)