Lesson 0031 · System Design · Modul Studi kasus · ±20 menit

Studi kasus: news feed (A)

Langkah 1–4: requirement, estimasi read-heavy, API dengan cursor, data model follow, dan desain v1 fan-out on read

Sebelumnya, lesson 0030 menyelesaikan satu soal dalam satu lesson. News feed lebih besar, jadi dibagi dua: lesson ini mengerjakan langkah 1–4 sampai desain v1, dan lesson 0032 mengerjakan langkah 5 (deep dive fan-out). Soal ini klasik karena jawabannya bergantung hampir sepenuhnya pada angka: desain yang tepat untuk 100 ribu user salah untuk 10 juta, dan sebaliknya.

Target lesson ini: Anda bisa menjawab "Rancang news feed: user mem-posting dan melihat post terbaru dari akun yang mereka follow" sampai desain v1, tanpa catatan, dalam 90 detik: requirement, QPS baca dan tulis (read-heavy 10:1), API dengan cursor, tabel posts dan follows, desain v1 fan-out on read, dan angka yang membuat v1 tidak cukup di skala target.

Langkah 1 — Klarifikasi requirement

Pertanyaan "berapa follower akun terbesar?" terlihat kecil, tetapi jawabannya menentukan deep dive di lesson 0032. Tanyakan di langkah 1, jangan menunggu interviewer menyebutnya.

Langkah 2 — Estimasi

BesaranHitunganHasil
QPS tulis (post)107 post ÷ 105 detik±100 QPS; puncak 3× ≈ ±300 QPS
QPS baca (feed)107 × 10 ÷ 105 detik±1.000 QPS; puncak ≈ ±3.000 QPS
Rasio baca:tulis1.000 ÷ 10010:1, read-heavy
Storage post107 × ±1 KB per hari±10 GB/hari ≈ ±3,6 TB/tahun
Biaya baca v13.000 feed/detik × 200 akun di-follow±600.000 pencarian index per detik

Rasio 10:1 menempatkan soal ini di keluarga read-heavy: deep dive akan berfokus pada jalur baca feed. Graf sosial di skala besar memang sangat timpang ke arah baca; paper TAO dari Facebook melaporkan bahwa "TAO can process a billion reads and millions of writes each second." [1] Baris terakhir tabel adalah angka yang paling penting: berapa pekerjaan database untuk satu pembukaan feed. Simpan angka itu; lesson 0032 dimulai dari sana.

Langkah 3 — API dan data model

POST /v1/posts                       { "text": "…" }   → 201 { "postId": 9001 }
POST /v1/users/77/follow                                → 204
GET  /v1/feed?limit=20&cursor=<created_at,id post terakhir>
  → 200 { "posts": [ … 20 post … ], "nextCursor": "2026-10-31T09:12:03Z,8995" }

Tabel
  posts    (id PK, author_id, text, created_at)
           INDEX (author_id, created_at DESC)
  follows  (follower_id, followee_id, created_at, PK (follower_id, followee_id))
           INDEX (followee_id)                       ← daftar follower, dipakai di lesson 0032

Kenapa cursor, bukan OFFSET. Dokumentasi PostgreSQL: "The rows skipped by an OFFSET clause still have to be computed inside the server; therefore a large OFFSET might be inefficient." [2] Feed juga terus bertambah di atas: dengan OFFSET 20, satu post baru menggeser semua halaman sehingga user melihat post yang sama dua kali. Cursor menyimpan posisi post terakhir (created_at, lalu id sebagai pemutus seri) dan halaman berikutnya mulai tepat dari sana.

Kenapa index (author_id, created_at). "A multicolumn B-tree index can be used with query conditions that involve any subset of the index's columns, but the index is most efficient when there are constraints on the leading (leftmost) columns." [3] Query feed selalu menyaring author_id, lalu mengurutkan menurut waktu, jadi urutan kolom itu cocok dengan bentuk query.

Latihan 1 — Estimasi dan biaya baca

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: fan-out on read. Saat post dibuat, hanya satu baris yang ditulis ke posts. Feed dirakit saat dibaca:

SELECT p.* FROM posts p
JOIN follows f ON f.followee_id = p.author_id
WHERE f.follower_id = :me
  AND (p.created_at, p.id) < (:cursor_time, :cursor_id)
ORDER BY p.created_at DESC, p.id DESC
LIMIT 20;

App server stateless di belakang load balancer, satu PostgreSQL. Tidak ada cache, queue, atau worker. Saat peluncuran (mis. 100 ribu DAU) puncak feed hanya ±30 QPS, atau ±6.000 pencarian index per detik. Itu wajar untuk dicoba di satu database; ukur dengan EXPLAIN ANALYZE sebelum menambah komponen apa pun. Desain ini juga benar secara otomatis: post yang dihapus atau akun yang di-unfollow langsung hilang dari feed, karena feed tidak pernah disalin.

Latihan 2 — Gambar diagram v1 sendiri dulu

Gambar diagram v1 Anda sendiri dulu: jalur tulis post dan jalur baca feed, dan di mana pekerjaan "merakit feed" terjadi. Setelah selesai, buka jawabannya.

Lihat jawaban
Client Load balancer App server ×N PostgreSQL stateless posts + follows INDEX (author_id, created_at) POST /v1/posts: 1 INSERT GET /v1/feed: 1 query JOIN
v1 fan-out on read: tulis murah (satu baris), baca mahal (satu query menyentuh 200 penulis). Cukup sampai angka baca menunjukkan sebaliknya.

Jalur tulis: POST /v1/posts → INSERT satu baris → 201. Jalur baca: GET /v1/feed → query di atas → 20 post + nextCursor.

Di mana v1 berhenti cukup. Di skala target, pekerjaan baca menjadi ±600.000 pencarian index per detik pada puncak, dan setiap pembukaan feed menggabungkan dan mengurutkan post dari 200 penulis. Read replica bisa membagi beban itu, tetapi tidak mengurangi pekerjaan per feed. Pertanyaannya: bisakah feed disiapkan sebelum dibaca?

Latihan 3 — API dan desain v1

Jawab tanpa scroll ke atas.

Latihan lisan — Ucapkan dengan suara keras

Pasang timer dan jawab dengan suara keras, seolah sedang interview. Jangan buka jawaban contoh sebelum selesai.

"Rancang news feed: user mem-posting dan melihat post terbaru dari akun yang mereka follow. Mulai dari requirement sampai desain v1." (90 detik)
Bandingkan dengan jawaban contoh dan checklist-nya

"Saya klarifikasi dulu: feed kronologis terbalik dari akun yang di-follow, tanpa ranking; 10 juta DAU, rata-rata follow 200 akun, ada akun besar dengan jutaan follower. Estimasinya: sekitar 100 post per detik dan sekitar 1.000 pembukaan feed per detik, puncak 3.000, jadi read-heavy 10:1. API-nya POST /v1/posts, POST follow, dan GET /v1/feed dengan cursor, bukan OFFSET, karena OFFSET besar tetap dihitung server dan halaman bergeser saat ada post baru. Data model: posts dengan index (author_id, created_at) dan follows (follower_id, followee_id). Desain v1-nya fan-out on read: app server stateless di belakang load balancer, satu query mengambil post terbaru dari akun yang di-follow, dari satu PostgreSQL. Itu cukup saat peluncuran; di skala target, 3.000 × 200 = 600.000 pencarian index per detik, dan itu yang saya serang di deep dive."

Checklist: apakah Anda menyebut…

  • requirement: feed kronologis dari akun yang di-follow, ranking di luar scope, akun besar disebut?
  • estimasi: ±100 post/detik, ±1.000 feed/detik (puncak ±3.000), read-heavy 10:1?
  • API: POST post, follow, GET feed dengan cursor, dan alasan tidak memakai OFFSET?
  • data model: posts dengan index (author_id, created_at) dan tabel follows?
  • v1 fan-out on read: app server stateless + satu database, satu query per pembukaan feed?
  • membenarkan komponen dengan angka (v1 cukup di awal; 600.000 pencarian index/detik di skala target jadi alasan deep dive)?

Lulus jika minimal 4 dari 6.

Bacaan lanjutan (opsional, tidak dihitung dalam durasi) Bronson et al., "TAO: Facebook's Distributed Data Store for the Social Graph", USENIX ATC 2013, ±40 menit. Bagaimana graf follow/teman dilayani di skala miliaran baca per detik.

Desain v1 sudah berdiri, dan angka 600.000 sudah di tangan. Lanjut di lesson 0032: langkah 5, fan-out on write vs on read, dan hybrid untuk akun besar.

Istilah: GLOSSARY.md (bagian "Studi kasus") · Indeks: semua lesson System Design

Ada yang belum jelas? Tanyakan ke saya (agent) di terminal, misalnya: "kenapa cursor memakai id sebagai pemutus seri?", "kapan read replica cukup untuk feed?", atau "bagaimana feed berubah kalau ada ranking?"

Sumber: [1] Bronson et al., TAO: Facebook's Distributed Data Store for the Social Graph (USENIX ATC 2013) · [2] PostgreSQL docs: LIMIT and OFFSET · [3] PostgreSQL docs: Multicolumn Indexes