Sistem notifikasi mengabarkan pengguna tentang acara yang tidak dilihatnya secara langsung, misalnya komentar baru atau perubahan status pekerjaan. Topik ini merancang sistem tersebut dengan kerangka studi kasus yang sama, yaitu kebutuhan dan estimasi, antarmuka, desain tingkat tinggi, pendalaman fan-out, template, dan deduplikasi, sampai analisis mode kegagalan. Kanal pengirimannya dipisahkan dari layanan sumber melalui queue, dan satu simulasi Python memperlihatkan bahwa deduplikasi menahan pengiriman ulang acara yang sama.
Kebutuhan dan estimasi#
Secara fungsional, satu acara dari layanan sumber menghasilkan notifikasi bagi banyak pengguna sekaligus, dan setiap pengguna memilih kanal yang disukainya, misalnya email atau push. Satu acara tidak boleh muncul dua kali bagi pengguna yang sama, dan pengiriman yang gagal dicoba ulang tanpa menciptakan salinan baru. Secara nonfungsional, keterlambatan beberapa menit masih dapat diterima karena notifikasi bukan percakapan, sehingga seluruh jalurnya boleh berjalan asinkron dan tidak ada permintaan pengguna yang menunggu pengiriman selesai.
Estimasinya memakai angka layanan berukuran sedang, yaitu 1.000.000 pengguna aktif yang menerima 10 notifikasi per hari, sehingga totalnya 10.000.000 notifikasi per hari atau sekitar 116 per detik pada rata-rata. Beban puncaknya jauh lebih tinggi karena acara besar memicu gelombang notifikasi sekaligus, misalnya lima kali laju normal, dan pola sesaat itulah yang menentukan kapasitas, bukan rata-ratanya. Kedua sifat ini, yaitu beban bertingkat dan toleransi jeda, menunjuk pada penyangga di tengah jalur alih-alih pengiriman langsung.
Antarmuka#
Layanan sumber tidak mengirim notifikasi, melainkan menerbitkan acara ke queue notifikasi. Acara memuat pengenalnya, jenisnya, pelakunya, dan daftar penerima, sebagaimana contoh berikut.
{"id": "komentar-42", "jenis": "komentar",
"oleh": "Budi", "artikel": "artikel-42",
"penerima": ["Ali", "Citra", "Dewi", "Eka"]}Daftar penerima ditentukan oleh layanan sumber, karena hanya pihak itu yang mengetahui relasi antara acara dan pengguna yang perlu tahu. Pekerja notifikasi tidak membaca basis data relasi sosial, sehingga tanggung jawabnya tetap sempit. Bentuk acara inilah kontrak antara layanan sumber dan pekerja notifikasi, sehingga perubahan isinya diperlakukan seperti perubahan API, dan daftar penerima pun cukup berupa pengenal pengguna karena preferensi kanal dibaca dari penyimpanannya saat fan-out. Pengenal acara pada contoh itu juga menjadi bagian dari kunci deduplikasi yang dipakai pada bagian selanjutnya. Tidak ada jawaban yang menunggu hasil pengiriman, karena penerbitan acara berakhir saat pesan tersimpan di queue, dan status pengirimannya dibaca dari penyimpanan status, bukan dari jawaban penerbitan.
Desain tingkat tinggi#
Desainnya tersusun atas lima komponen, yaitu layanan sumber sebagai producer, queue notifikasi sebagai penyangga, pekerja notifikasi yang melakukan fan-out dan deduplikasi, adapter per kanal, dan penyimpanan status notifikasi.
Queue memutus ketergantungan dua arah, sehingga layanan sumber tidak menunggu pengiriman dan gelombang acara terserap sebagai penyangga yang dikerjakan bertahap. Adapter kanal membungkus perbedaan protokol tiap kanal di belakang antarmuka yang sama, sehingga pekerja tidak mengetahui cara email sungguhan dikirim. Penyimpanan status mencatat setiap notifikasi dengan keadaan antre, kirim, atau gagal, sehingga pengiriman dapat dilacak dan diulang dengan sasaran yang jelas. Kapasitas pekerja dinaikkan dengan menambah instance yang menarik dari queue yang sama, karena keputusan fan-out memerlukan keadaan yang kecil, yaitu daftar penerima, preferensi, dan kunci deduplikasi. Saat trafik melonjak, queue menahan ketinggalan itu dan pekerja mengejarnya tanpa mengubah layanan sumber.
Template dan kanal#
Tiap kanal menuntut bentuk yang berbeda, karena email memiliki subjek dan isi panjang, sms dibatasi beberapa puluh karakter, dan push hanya satu baris di layar kunci. Template, yaitu pola teks dengan tempat kosong yang diisi saat pengiriman, memisahkan kalimat tersebut dari kode, sehingga perubahan kata tidak menyentuh logika fan-out. Variabelnya diisi langsung dari isi acara.
TEMPLATE = {
"email": "{oleh} berkomentar pada {artikel}",
"sms": "{oleh} berkomentar di {artikel}",
"push": "Komentar baru dari {oleh}",
}
pesan = TEMPLATE["sms"].format(oleh="Budi", artikel="artikel-42")Pengenal seperti artikel-42 pada contoh adalah label internal, sedangkan teks untuk pengguna seharusnya memuat judul asli artikel yang dikirim bersama acara. Template disusun per jenis acara dan per kanal, misalnya komentar, perubahan status, dan pengingat jadwal masing-masing berupa satu set kecil. Pemisahan ini juga memberi tempat yang jelas untuk kalimat yang berbeda antarkanal, seperti subjek email yang lebih panjang daripada baris push. Pola ini pada akhirnya membuat penambahan kanal baru hanya menuntut satu adapter dan satu template, tanpa mengubah pekerja notifikasi sama sekali.
Fan-out dan deduplikasi#
Pekerja notifikasi menerima acara lalu membentuk satu notifikasi untuk setiap penerima, dengan kanal yang diambil dari preferensi masing-masing pengguna. Queue bekerja dengan semantik at-least-once, yaitu menyerahkan acara yang sama lebih dari sekali saat pengiriman ulang terjadi, sehingga pekerja wajib menahan salinannya. Kunci deduplikasinya adalah pasangan pengguna dan pengenal acara, dan entri kuncinya diberi TTL agar tidak menumpuk selamanya.
"""Simulasi fan-out satu acara ke banyak pengguna dengan deduplikasi.
Kanal email, sms, dan push hanya berupa nama kanal, bukan layanan nyata.
Setiap pengguna memilih satu kanal penerimaan, sehingga satu acara
menghasilkan tepat satu notifikasi per pengguna. Antrean menyerahkan
acara yang sama dua kali untuk meniru pengiriman ulang pada semantik
at-least-once, dan deduplikasi berdasarkan pasangan (pengguna, acara)
menahan duplikatnya.
"""
PREFERENSI_KANAL = {
"Ali": "email",
"Citra": "email",
"Dewi": "push",
"Eka": "sms",
}
TEMPLATE = {
"email": "{oleh} berkomentar pada {artikel}",
"sms": "{oleh} berkomentar di {artikel}",
"push": "Komentar baru dari {oleh}",
}
def proses_acara(acara, sudah_dikirim):
kirim = 0
lewati = 0
for pengguna, kanal in PREFERENSI_KANAL.items():
kunci = (pengguna, acara["id"])
if kunci in sudah_dikirim:
lewati += 1
print(f" duplikat dilewati: {pengguna} / {kanal} / {acara['id']}")
continue
sudah_dikirim.add(kunci)
pesan = TEMPLATE[kanal].format(**acara)
print(f" antri {kanal:<5} -> {pengguna}: {pesan}")
kirim += 1
return kirim, lewati
if __name__ == "__main__":
acara = {"id": "komentar-42", "jenis": "komentar",
"oleh": "Budi", "artikel": "artikel-42"}
sudah_dikirim = set()
print("penyerahan pertama (pengiriman normal):")
kirim, lewati = proses_acara(acara, sudah_dikirim)
print(f" total kirim {kirim}, lewati {lewati}")
print("penyerahan kedua (antrean mengirim ulang acara yang sama):")
kirim, lewati = proses_acara(acara, sudah_dikirim)
print(f" total kirim {kirim}, lewati {lewati}")penyerahan pertama (pengiriman normal):
antri email -> Ali: Budi berkomentar pada artikel-42
antri email -> Citra: Budi berkomentar pada artikel-42
antri push -> Dewi: Komentar baru dari Budi
antri sms -> Eka: Budi berkomentar di artikel-42
total kirim 4, lewati 0
penyerahan kedua (antrean mengirim ulang acara yang sama):
duplikat dilewati: Ali / email / komentar-42
duplikat dilewati: Citra / email / komentar-42
duplikat dilewati: Dewi / push / komentar-42
duplikat dilewati: Eka / sms / komentar-42
total kirim 0, lewati 4Keluarannya memperlihatkan empat pengiriman pada penyerahan pertama, yaitu satu notifikasi per pengguna dengan kanal yang berbeda-beda, lalu empat penahanan pada penyerahan kedua. Kunci deduplikasi memuat pengguna karena penerima yang berbeda tidak boleh saling meniadakan, dan memuat pengenal acara karena acara berikutnya tetap boleh mengabari pengguna yang sama. Kunci itu juga tidak menggabungkan dua acara yang berbeda, karena pengenal acaranya berbeda, sehingga dua pengabaran yang memang seharusnya terpisah tetap terkirim. Dalam implementasi, kunci itu disimpan bersama TTL yang cukup panjang untuk menutup seluruh masa pengiriman ulang queue, misalnya beberapa hari.
Mode kegagalan#
- Adapter kanal gagal sementara. Pengiriman dicoba ulang dengan jeda yang memanjang, dan pesan yang tetap gagal setelah batas dipindahkan ke queue dead letter untuk ditangani terpisah. Kegagalan satu kanal tidak menggagalkan notifikasi di kanal lain.
- Pekerja mati setelah mengirim tetapi sebelum mencatat status. Queue menyerahkan acara yang sama kembali, dan deduplikasi menahan notifikasi dobelnya, sehingga pengguna hanya melihat satu salinan.
- Queue notifikasi mati. Layanan sumber tidak dapat menerbitkan acara, sehingga notifikasi tertunda, tetapi alur tulis yang memicu acara tetap selesai. Producer diberi percobaan ulang penerbitan agar jeda ini tidak menghilangkan acara.
- Penyimpanan kunci deduplikasi mati. Pengiriman ulang dari queue tidak lagi tertahan, dan pengguna dapat menerima notifikasi dobel selama gangguan berlangsung, sehingga penyimpanan kunci diberi cadangan seperti penyimpanan status yang lain.
- Template gagal dirender karena variabel yang hilang. Satu acara tidak terkirim dan kegagalannya dicatat, sedangkan pekerja melanjutkan acara berikutnya, sehingga kesalahan teks tidak menjatuhkan seluruh alur.
Kesalahan umum#
- Mengirim notifikasi langsung dari alur permintaan tulis. Latensi aksi pengguna menjadi bergantung pada layanan email atau sms pihak kanal, dan kegagalan pengiriman ikut menggagalkan aksinya. Perbaikannya adalah menerbitkan acara ke queue dan mengirimnya dari pekerja.
- Menyimpan kunci deduplikasi tanpa TTL. Tabel kunci tumbuh tanpa batas meskipun masa pengiriman ulang hanya beberapa hari, sehingga kuncinya diberi masa hidup sesuai semantik queue.
- Mengabaikan preferensi kanal pengguna. Notifikasi yang tidak diinginkan mendorong pengguna mematikan seluruh notifikasi, dan preferensi itu diperiksa pada saat fan-out, bukan saat pendaftaran.
- Memakai satu template untuk semua kanal. Pesan email yang dipaksa ke sms terpotong di tengah kalimat, sehingga tiap kanal memperoleh template dengan panjang yang sesuai.
- Menggabungkan seluruh jenis acara ke satu queue tanpa prioritas. Notifikasi yang mendesak tertunda di belakang gelombang acara massal, sehingga jenis acara dengan kebutuhan waktu yang berbeda dipisahkan atau diberi prioritas.
Ringkasan#
- Layanan sumber hanya menerbitkan acara ke queue, sedangkan pekerja notifikasi melakukan fan-out ke penerima, merender template per kanal, dan menyimpan status pengiriman.
- Deduplikasi memakai kunci pasangan pengguna dan pengenal acara dengan TTL, sehingga semantik at-least-once dari queue tidak pernah menghasilkan notifikasi dobel bagi pengguna.
- Adapter per kanal membungkus perbedaan protokol di belakang antarmuka yang sama, sehingga penambahan kanal tidak mengubah pekerja, dan kegagalan satu kanal terisolasi.
Referensi#
- Retry pattern — Microsoft Learn — Strategi mengulang operasi yang gagal sesaat, termasuk syarat idempotensi pada operasinya.
- Dead Letter Exchanges — RabbitMQ — Penampung pesan yang gagal diproses berulang kali pada sebuah queue.