Studi kasus: notification system
Push, SMS, dan email: queue per channel, retry dengan backoff, dead letter queue, idempotency, dan preferensi user
Sebelumnya, lesson 0029 merancang komponen sinkron yang ada di setiap request. Notification system adalah kebalikannya: hampir semuanya asinkron, bergantung pada provider eksternal yang bisa lambat atau gagal, dan pertanyaan utamanya adalah "bagaimana tidak kehilangan notifikasi tanpa mengirimnya dua kali".
Langkah 1 — Klarifikasi requirement
- Functional: service lain (order, pembayaran) dan tim marketing (kampanye) memicu notifikasi; channel: push iOS/Android, SMS, email; user bisa mematikan channel tertentu (preferensi). Di luar scope: membuat template, A/B test.
- Skala (asumsi): 10 juta notifikasi per hari; kampanye terbesar 5 juta user dalam 10 menit.
- Non-functional: notifikasi transaksi ("pesanan dikirim") sampai dalam hitungan detik, kampanye boleh menit; tidak boleh hilang; sebisa mungkin tidak dobel; preferensi user wajib dihormati.
Push ke ponsel tidak dikirim langsung ke perangkat, tetapi lewat layanan platform: APNs (Apple) untuk iOS dan Firebase Cloud Messaging (FCM) untuk Android/web. FCM menyebut dirinya "a cross-platform messaging solution that lets you reliably send messages", dengan payload pesan sampai 4096 byte. [1] SMS dan email juga lewat provider pihak ketiga. Semua provider ini berada di luar kendali Anda.
Langkah 2 — Estimasi
| Besaran | Hitungan | Hasil |
|---|---|---|
| QPS rata-rata | 107 ÷ 105 detik | ±100 notifikasi/detik |
| QPS puncak kampanye | 5 × 106 ÷ 600 detik | ±8.300 notifikasi/detik (±80× rata-rata) |
| Log pengiriman | 107 × ±500 byte per hari | ±5 GB/hari; simpan 90 hari ≈ ±450 GB |
| Provider (asumsi, tanyakan) | kuota akun SMS | mis. ±100 SMS/detik, jauh di bawah puncak |
Dua angka ini membenarkan queue sejak v1: puncak 80× rata-rata, dan provider lebih lambat daripada laju masuk. Queue meratakan beban (load leveling): pemanggil langsung mendapat jawaban, worker mengirim secepat yang diizinkan provider. Kalau sistemnya hanya mengirim beberapa ribu email transaksi per hari tanpa kampanye, satu background job sederhana yang memanggil provider sudah cukup.
Langkah 3 — API dan data model
POST /v1/notifications
{ "idempotencyKey": "order-8812-shipped", "userId": 42,
"type": "order_shipped", "data": { "orderId": 8812 } }
→ 202 Accepted { "notificationId": "n_93f…" } ← diterima, dikirim asinkron
PUT /v1/users/42/preferences { "push": true, "sms": false, "email": true }
Tabel
notifications (id PK, idempotency_key UNIQUE, user_id, type, created_at)
deliveries (notification_id, channel, status, attempts, PK (notification_id, channel))
preferences (user_id, channel, enabled)
device_tokens (user_id, platform, token) ← alamat push per perangkat
idempotencyKey dibuat oleh pemanggil, mengikuti pola yang dipakai API publik seperti Stripe: kunci unik yang
"the server uses to recognize subsequent retries of the same request", sehingga request bisa diulang "without accidentally performing the same operation twice". [2]
Constraint UNIQUE di kolom itu membuat retry dari order service tidak membuat notifikasi kedua.
Langkah 4 — High-level design (v1)
Desain v1 paling sederhana yang memenuhi angka: notification service menerima request, mengecek preferensi, menyimpan baris di database, lalu memasukkan satu pesan per channel ke satu queue. Satu kelompok worker mengambil pesan dan memanggil provider. Preferensi dicek sebelum enqueue, supaya pesan yang dilarang user tidak pernah masuk antrean.
Latihan 1 — Gambar diagram v1 sendiri dulu
Gambar diagram v1 Anda sendiri dulu: siapa memanggil siapa, di mana preferensi dicek, dan bagian mana yang sinkron atau asinkron. Setelah selesai, buka jawabannya.
Lihat jawaban
Alur: POST → cek idempotency key dan preferensi → simpan notifications + deliveries → enqueue → 202. Worker: ambil pesan → panggil provider → tandai deliveries.status = sent → hapus pesan dari queue.
Langkah 5 — Deep dive, bottleneck, trade-off
Evolusi dari v1 ke v2, satu perubahan per masalah yang terlihat di angka atau metrik:
1. Queue per channel. Di v1, kalau provider SMS melambat (kuota ±100/detik) saat kampanye, pesan SMS menumpuk dan ikut menahan push dan email di queue yang sama. Pisahkan menjadi satu queue + worker per channel. Setiap channel lalu bisa diskalakan dan dibatasi lajunya sesuai kuota provider-nya sendiri, dan gangguan satu provider tidak merembet. Biayanya: tiga queue dan tiga kelompok worker untuk dipantau.
2. Retry dengan exponential backoff + jitter. Kegagalan provider sering sementara, jadi retry. Tapi retry serentak dari ribuan worker memperparah beban. AWS Builders' Library: "Jitter adds some amount of randomness to the backoff to spread the retries around in time." [3] Kegagalan permanen (nomor tidak valid, token perangkat kedaluwarsa) tidak di-retry; tandai gagal dan hapus token-nya.
3. Dead letter queue. Pesan yang terus gagal tidak boleh berputar selamanya. Di Amazon SQS, maxReceiveCount adalah
"the number of times a consumer can receive a message from a source queue before it is moved to a dead-letter queue", dan nilainya perlu cukup tinggi untuk memberi ruang retry. [4]
DLQ dipantau dan bisa diproses ulang setelah penyebabnya diperbaiki.
4. Idempotency di worker. Queue standar biasanya at-least-once: SQS menyatakan Anda "might get that message copy again" dan meminta aplikasi
"to be idempotent (they should not be affected adversely when processing the same message more than once)". [5]
Worker mengecek deliveries (kunci notification_id + channel) sebelum mengirim, dan melewati pesan yang sudah sent.
Trade-off yang perlu Anda ucapkan: kalau worker crash setelah provider menerima tetapi sebelum status ditulis, pesan bisa terkirim dua kali. Menandai sebelum mengirim membalik risikonya menjadi pesan hilang.
Untuk notifikasi, dobel sesekali biasanya lebih bisa diterima daripada hilang.
5. Preferensi dan batas per user. Pada puncak ±8.300/detik, setiap notifikasi membaca preferensi. Ukur dulu; kalau database preferensi menjadi bottleneck, cache preferensi (jarang berubah). Batasi juga kampanye per user (mis. maksimal 3 promosi per hari) dengan counter seperti rate limiter 0029.
Latihan 2 — Queue, retry, dan idempotency
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.
Latihan lisan — Ucapkan dengan suara keras
Pasang timer dan jawab dengan suara keras, seolah sedang interview. Jangan buka jawaban contoh sebelum selesai.
"Rancang notification system untuk push, SMS, dan email: 10 juta notifikasi per hari, kampanye sampai 5 juta user dalam 10 menit." (90 detik)
Bandingkan dengan jawaban contoh dan checklist-nya
"Rata-ratanya sekitar 100 notifikasi per detik, tapi kampanye membuat puncak sekitar 8.300 per detik, dan provider SMS mungkin hanya menerima sekitar 100 per detik. Karena itu queue sudah dibenarkan di v1: notification service mengecek idempotency key dan preferensi user, menyimpan baris notifikasi, memasukkan pesan ke queue, dan langsung membalas 202; worker memanggil FCM/APNs, provider SMS, dan email. Di v2 saya pisahkan queue dan worker per channel, karena provider SMS yang lambat tidak boleh menahan push. Kegagalan sementara di-retry dengan exponential backoff dan jitter; setelah beberapa kali gagal, pesan masuk dead letter queue. Queue-nya at-least-once, jadi worker harus idempotent: cek status delivery per notifikasi dan channel sebelum mengirim. Trade-off-nya: crash setelah kirim sebelum mencatat bisa menyebabkan dobel, dan untuk notifikasi itu lebih baik daripada hilang."
Checklist: apakah Anda menyebut…
- estimasi: ±100/detik rata-rata, puncak kampanye ±8.300/detik, kuota provider?
- queue antara notification service dan worker, dengan respons 202 asinkron?
- queue per channel supaya satu provider yang lambat tidak menahan channel lain?
- retry dengan exponential backoff + jitter, lalu dead letter queue?
- idempotency key dari pemanggil dan worker idempotent karena at-least-once; preferensi dicek sebelum enqueue?
- membenarkan komponen dengan angka (queue karena puncak 80× dan kuota provider; queue per channel karena gangguan satu provider)?
Lulus jika minimal 4 dari 6.
Berikutnya, lesson 0031: news feed, langkah 1–4.
Istilah: GLOSSARY.md (bagian "Studi kasus") · Indeks: semua lesson System Design
Ada yang belum jelas? Tanyakan ke saya (agent) di terminal, misalnya: "bagaimana menjadwalkan notifikasi agar tidak terkirim tengah malam?", "bagaimana menangani token push yang kedaluwarsa?", atau "kapan butuh prioritas antara notifikasi transaksi dan kampanye?"
Sumber: [1] Firebase docs: Firebase Cloud Messaging · [2] Stripe API reference: Idempotent requests · [3] AWS Builders' Library: Timeouts, retries, and backoff with jitter · [4] Amazon SQS docs: Dead-letter queues · [5] Amazon SQS docs: At-least-once delivery