Message Queue — System Design

Message Queue adalah komponen yang memungkinkan komunikasi asinkron antar service. Producer mengirim pesan ke queue, consumer memproses pesan dari queue…

Message queue adalah antrean pesan di antara dua bagian sistem: yang satu menaruh pekerjaan, yang lain mengambil dan mengerjakannya nanti, tanpa saling menunggu. Bayangin dapur restoran. Kasir menempel struk pesanan di rel, lalu langsung melayani pelanggan berikutnya. Koki mengambil struk, memasak, lalu mencoret struknya. Kalau koki pingsan sebelum mencoret, struknya masih di rel, dan koki lain akan memasaknya lagi.

🧪 Tebakan cepat: worker TiketNada mengirim e-tiket pesanan #1141, lalu mati sebelum sempat bilang "selesai" ke queue. 10 detik kemudian, apa yang terjadi: pesannya hilang, e-tiketnya terkirim lagi, atau queue-nya berhenti? Tebak dulu, jawabannya ada di bagian "Ack, visibility timeout, dan at-least-once".

Producer, queue, consumer

Yang masuk queue adalah pekerjaan yang boleh selesai belakangan. Saat user mendaftar, validasi format email dan menyimpan user ke database harus selesai di request itu juga, karena user perlu tahu hasilnya. Mengirim email selamat datang boleh lewat queue: user gak perlu menunggu emailnya terkirim untuk lanjut.

Kenapa pakai queue: decoupling dan menyerap lonjakan

Masalah utama yang diselesaikan queue adalah decoupling: producer gak perlu menunggu consumer selesai. Model contoh di Lab: jam 10.00, war tiket, 40 pembeli membayar bersamaan. API TiketNada punya 8 worker (proses yang melayani request).

Tanpa queue Dengan queue
Request bayar membuat e-tiket di dalam request: 5 + 500 ms menaruh job di queue: 5 ms
40 pembeli sekaligus 5 putaran x 505 ms: pembeli ke-40 menunggu 2.525 ms semua dijawab dalam 5 ms
E-tiket jadi bersamaan dengan response 2 worker x 2 per detik: habis dalam 10 detik
Pengiriman email gagal pembeli melihat error 500 padahal sudah bayar job dicoba lagi diam-diam
Semua worker mati (gak ada worker terpisah) API tetap 5 ms, job menumpuk aman di queue

Lonjakan 40 pesanan "diserap" queue lalu dikerjakan dengan tempo yang sanggup ditangani worker. Kalau worker lambat atau mati sebentar, pesannya tetap aman menunggu.

Ack, visibility timeout, dan at-least-once

Saat worker mengambil pesan, queue gak langsung menghapusnya. Pesan itu cuma disembunyikan selama visibility timeout (10 detik di Lab; bawaan Amazon SQS 30 detik). Kalau worker mengirim ack sebelum waktunya habis, pesan dihapus. Kalau gak, queue menganggap worker-nya mati dan memunculkan pesan itu lagi untuk worker lain.

Ini yang terjadi di Lab saat worker mati setelah mengirim e-tiket tapi sebelum ack:

Detik Kejadian
1 worker mengambil #1141, mengirim e-tiket ke-1, lalu mati sebelum ack
1 sampai 11 #1141 tersembunyi, menunggu visibility timeout
11 timeout habis, #1141 muncul lagi, worker lain mengirim e-tiket ke-2

E-tiketnya terkirim lagi. Itu jawaban tebakannya. Pesan gak hilang, tapi bisa dikerjakan lebih dari sekali. Jaminan ini disebut at-least-once delivery: setiap pesan pasti dikerjakan, minimal sekali. Kebanyakan queue memberi jaminan ini, karena "tepat sekali" di jaringan yang bisa putus kapan saja sangat sulit.

Consumer harus idempoten

Karena pesan bisa datang dua kali, consumer harus idempoten: mengerjakan pesan yang sama dua kali hasilnya sama dengan sekali. Caranya: cek dulu apakah pekerjaan untuk orderId itu sudah pernah selesai.

const IDEMPOTENT = true;
const eTiket = new Map(); // orderId -> berapa e-tiket terkirim

function handle(job) {
  if (IDEMPOTENT && eTiket.has(job.orderId)) {
    console.log('#' + job.orderId + ' sudah punya e-tiket, lewati');
    return; // tetap ack, tapi gak kirim lagi
  }
  kirimETiket(job); // bikin PDF, kirim email, catat di eTiket
}

Tanpa cek ini, pesan yang datang dua kali berarti dua e-tiket, dua email, atau lebih parah, dua kali potong saldo. Pola ini dibahas lebih dalam di lesson Idempotency Design.

Retry dan dead letter queue

Kalau pekerjaan gagal (exception), pesannya kembali ke queue dan dicoba lagi. Tapi ada pesan yang selalu gagal, misalnya pesanan #1142 dengan email tono@ yang gak valid. Pesan seperti ini disebut poison message. Kalau dicoba terus tanpa batas, dia memakan jatah worker selamanya.

Jadi retry diberi batas. Di Lab: dicoba lagi 2 detik setelah gagal, maksimal 3 kali (detik 1, 3, dan 5), lalu dipindah ke dead letter queue (DLQ): queue khusus untuk pesan yang gagal terus. Pesan lain tetap jalan, dan pesan di DLQ menunggu diperiksa manusia. Di Amazon SQS batasnya maxReceiveCount, di Laravel php artisan queue:work --tries=3 (gagalnya masuk tabel failed_jobs), di BullMQ opsi attempts.

Kafka, RabbitMQ, dan teman-temannya

RabbitMQ Kafka
Bentuk broker pesan log event yang hanya ditambah di ujung
Setelah dikerjakan pesan dihapus setelah ack pesan tetap disimpan sesuai retention (misalnya 7 hari)
Replay gak bisa bisa, consumer membaca ulang dari posisi mana pun
Cocok untuk task queue klasik, routing yang rumit event streaming bervolume sangat tinggi

Pilihan lain: Amazon SQS (dikelola cloud, gak perlu urus server), dan Redis dengan BullMQ (sederhana, cocok kalau kamu sudah punya Redis).

⚠️ Jebakan yang sering ketemu

TL;DR: Message queue memisahkan producer dan consumer (decoupling): producer menaruh pesan lalu langsung lanjut, consumer mengerjakannya saat sanggup. Di model TiketNada, API menjawab 40 pembeli dalam 5 ms dan queue menyerap lonjakannya, bukan pembeli ke-40 menunggu 2.525 ms. Kerjakan lewat queue hanya yang boleh selesai belakangan, seperti email e-tiket atau email selamat datang. Pesan yang diambil disembunyikan selama visibility timeout; tanpa ack, pesan muncul lagi, jadi pengiriman at-least-once dan consumer wajib idempoten. Pesan yang gagal dicoba ulang dengan batas, lalu dipindah ke dead letter queue. Point-to-point untuk task, pub/sub untuk event. RabbitMQ menghapus pesan setelah ack dan kuat di routing; Kafka menyimpan log event yang bisa di-replay dengan throughput sangat tinggi.

Yang akan kamu pelajari