Message Broker: Kafka & RabbitMQ
Bayangkan sebuah toko online sederhana. Saat pelanggan menekan tombol "Beli", server mencatat order di database, lalu... harus mengirim email konfirmasi, mengurangi stok, memberi notifikasi ke aplikasi mobile, dan memperbarui laporan penjualan. Kalau semua itu dilakukan sinkron lewat HTTP, satu permintaan POST /orders bisa lambat karena menunggu email terkirim, stok ter-update, dan notifikasi terkirim — padahal yang penting bagi pelanggan hanyalah "order-nya tercatat". Ini masalah klasik arsitektur sinkron yang kaku.
Solusinya adalah message broker — middleware yang menjadi "kotak pos" antar service. Service yang menghasilkan kejadian (misalnya order.created) cukup menitipkan pesan ke broker, lalu service lain mengambil dan memprosesnya secara mandiri. Artikel ini membahas dua broker paling populer di dunia Java: Apache Kafka dan RabbitMQ, lengkap dengan pola, kode, dan kapan memakai yang mana.
Kenapa Perlu Message Broker
Dengan HTTP sinkron, semua panggilan antar service berjalan satu per satu dan menunggu jawaban. Masalahnya:
- Latensi membengkak.
POST /ordersmenunggu email terkirim (bisa ratusan milidetik) padahal pelanggan cuma ingin order tercatat. - Coupling antar service. Service order harus tahu alamat service email, stok, dan notifikasi. Menambah satu consumer baru berarti mengubah kode order.
- Tidak ada buffering. Kalau service email sedang down atau lambat, request order ikut gagal atau menumpuk di memori server.
- Sulit dikirim ke banyak penerima. Satu event hanya bisa dikirim ke satu endpoint; kalau butuh 5 consumer, harus 5 panggilan HTTP.
Message broker memecah masalah itu dengan decoupling: publisher tidak perlu tahu siapa konsumennya, konsumen bisa banyak, dan pesan disimpan sementara di broker sehingga konsumen yang sedang sibuk bisa memprosesnya belakangan — ini sekaligus menjadi buffer saat beban puncak. Bukannya memanggil service lain satu per satu, service cukup menulis satu pesan ke broker:
Tanpa broker (sinkron, kaku)
OrderService --HTTP--> EmailService (menunggu selesai)
\ --HTTP--> StockService (menunggu selesai)
\ --HTTP--> NotifService (menunggu selesai)
v
(lambat & error salah satu -> gagal semua)
Dengan broker (async, decoupled)
OrderService --pesan--> [BROKER]
|
+-------------------+-------------------+
| | |
EmailService StockService NotifService
Inti decoupling: service order cukup menulis satu pesan ke broker dan langsung selesai. Service lain yang menaruh minat pada event
order.createdakan memprosesnya sendiri-sendiri, kapan pun mereka siap.
Pola Dasar: Queue vs Publish-Subscribe
Ada dua pola komunikasi pesan yang wajib dipahami. Perbedaannya ada di jumlah penerima pesan:
| Aspek | Point-to-point Queue | Publish-Subscribe |
|---|---|---|
| Model | Satu pesan → satu consumer | Satu event → semua subscriber |
| Konsumen | Bersaing (competitor) memperebutkan pesan | Berlangganan (subscribe) pada topic |
| Contoh | Task queue, email worker, pembayaran | Event order.created ke banyak service |
| Jika consumer mati | Pesan antre, diproses worker lain | Event tetap tersimpan, bisa dibaca lagi |
Point-to-point: 1 pesan -> 1 consumer
[Queue] ---> Worker A
\--> (Worker B hanya menangkap
jika Worker A sibuk)
Publish-Subscribe: 1 event -> semua subscriber
[Topic/Exchange] ---> Consumer Email
---> Consumer Stock
---> Consumer Notifikasi
Kafka dioptimalkan untuk pola publish-subscribe, sedangkan RabbitMQ menguasai keduanya (queue klasik untuk work queue, exchange + fanout untuk publish-subscribe). Kita bahas satu per satu.
Apache Kafka: Log Terdistribusi yang Replayable
Kafka diciptakan LinkedIn untuk menangani aliran event dalam skala besar. Bedanya dari message broker pada umumnya: Kafka bukan sekadar antrean yang pesannya hilang setelah dikonsumsi, melainkan log persisten yang bisa dibaca ulang berkali-kali.
Konsep Inti Kafka

| Konsep | Penjelasan |
|---|---|
Producer | Aplikasi yang menulis pesan (event) ke topic |
Topic | Kategori pesan, misalnya order.created atau user.login |
Partition | Unit paralelisme dalam topic. Pesan dipecah ke beberapa partition; urutan dijamin per partition, bukan lintas partition |
Offset | Nomor posisi sebuah pesan di dalam partition. Konsumen menandai offset yang sudah dibaca |
Consumer | Aplikasi yang membaca pesan dari partition |
Consumer Group | Kumpulan consumer yang berbagi beban: satu partition dibaca maksimal oleh satu consumer dalam group yang sama |
Broker | Satu server Kafka. Kumpulan broker disebut cluster |
| Retention | Kebijakan berapa lama pesan disimpan, misalnya 7 hari, sebelum dibuang |
Poin penting dari tabel di atas: urutan hanya dijamin dalam satu partition. Kalau urutan event penting (misalnya payment.created harus dibaca sebelum payment.refunded), pastikan pesan dengan "kunci" yang sama (misal orderId) diarahkan ke partition yang sama. Kafka menentukan partition lewat hash dari key producer.
// Producer menulis event order.created ke topic yang sama.
// Pesan dengan orderId yang sama dijamin masuk partition yang sama.
kafkaTemplate.send("order.created", order.getId().toString(), order);
Kenapa Kafka "Replayable"?
Pada antrean biasa (queue), pesan dihapus begitu berhasil diproses oleh satu consumer — pesan hanya bisa dikonsumsi sekali. Kafka beda: pesan disimpan sebagai log di disk (dengan retention, misalnya 7 hari atau berdasarkan ukuran), dan setiap consumer membaca dari offset-nya sendiri. Artinya:
- Consumer baru bisa membaca semua pesan dari awal (offset 0).
- Consumer yang sudah selesai bisa reset offset dan membaca ulang — misalnya saat ada bug di kode, lalu bug diperbaiki.
- Beberapa consumer group bisa membaca topic yang sama secara independen, masing-masing dengan posisinya sendiri.
Inilah yang membuat Kafka menjadi "kolam data" untuk analitik: data yang masuk hari ini masih bisa diproses lagi besok untuk rekomputasi, tanpa minta producer mengirim ulang.
Queue dikonsumsi sekali, log dibaca ulang. Ini perbedaan filosofis paling mendasar antara queue model RabbitMQ dan log model Kafka.
Kapan Memakai Kafka
Kafka cocok bila kebutuhan Anda termasuk salah satu berikut:
- Aliran event besar (juta pesan per detik) dengan throughput tinggi.
- Log & audit trail: semua aksi user tersimpan untuk inspeksi/analitik.
- Streaming: agregasi, windowing, rekomputasi data (misalnya menghitung total penjualan per menit).
- Banyak consumer independent membaca topik yang sama untuk tujuan berbeda.
- Butuh replay: data harus bisa dibaca ulang setelah diproses.
Contoh Kode Spring Kafka
Spring Kafka menyembunyikan kerumitan Kafka di balik annotation. Consumer cukup satu method dengan @KafkaListener:
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
@Component
public class OrderConsumer {
@KafkaListener(topics = "order.created", groupId = "email-service")
public void onOrderCreated(OrderCreatedEvent event) {
// Dipanggil setiap kali ada pesan baru di topic order.created
// untuk groupId "email-service".
emailService.sendConfirmation(event.getEmail(), event.getOrderId());
}
}
Kalau Anda perlu manual acknowledgement (misalnya menunggu proses async selesai sebelum menganggap pesan berhasil), gunakan Acknowledgment dan set mode listener menjadi MANUAL di konfigurasi factory:
@KafkaListener(topics = "order.created", groupId = "email-service")
public void onOrderCreated(OrderCreatedEvent event, Acknowledgment ack) {
emailService.sendAsync(event).whenComplete((ok, err) -> {
if (err == null) {
ack.acknowledge(); // baru dianggap sukses setelah email terkirim
}
});
}
Satu topik, beberapa consumer group → masing-masing mendapat salinan semua pesan. Dua consumer dalam satu group → pesan terbagi (berkompetisi), dan paralelisme ditentukan jumlah partition.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
@Service
public class OrderPublisher {
private final KafkaTemplate<String, Object> kafkaTemplate;
public OrderPublisher(KafkaTemplate<String, Object> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
public void publish(Order order) {
OrderCreatedEvent event = new OrderCreatedEvent(
order.getId(), order.getEmail(), order.getTotal());
// key = orderId -> menjamin urutan per order di partition yang sama
kafkaTemplate.send("order.created", order.getId().toString(), event);
}
}
RabbitMQ: Routing Fleksibel untuk Task Queue
RabbitMQ menerapkan AMQP (Advanced Message Queuing Protocol). Beda besar dengan Kafka: di RabbitMQ, pesan tidak ditulis ke consumer secara langsung, melainkan melalui exchange yang bertugas merutekan pesan ke satu atau banyak queue.
Konsep Inti RabbitMQ
| Konsep | Penjelasan |
|---|---|
Producer | Aplikasi yang mengirim pesan ke exchange |
Exchange | "Pos routing": menerima pesan lalu memutuskan ke queue mana pesan diteruskan |
Queue | Antrean berisi pesan yang menunggu diproses consumer |
Binding & Routing Key | Aturan yang menghubungkan exchange ke queue; routing key adalah "alamat" yang dicocokkan binding |
Consumer | Aplikasi yang mengambil pesan dari queue |
Kunci memahami RabbitMQ adalah exchange. Ada empat jenis exchange dengan cara routing yang berbeda:
| Jenis Exchange | Cara Routing | Cocok untuk |
|---|---|---|
direct | Pesan dikirim ke queue yang binding-nya persis sama dengan routing key | Job spesifik, misalnya task.email |
topic | Routing key dicocokkan dengan pola wildcard (* satu kata, # banyak kata) | Routing berjenjang, misal order.created.eu |
fanout | Pesan disalin ke semua queue yang ter-binding (mengabaikan routing key) | Broadcast / publish-subscribe |
headers | Cocokkan berdasarkan header pesan, bukan routing key | Routing dengan banyak kondisi |
Contoh topic exchange: binding email.* akan menerima email.welcome dan email.reset, tapi tidak menerima order.created. Binding #.created menerima order.created, payment.created, dan seterusnya — ini memberi fleksibilitas routing yang tidak dimiliki Kafka.
Consumer / Worker Queue
Di RabbitMQ, queue biasa melayani pola work queue: satu pesan diproses oleh tepat satu worker. Pesan dikeluarkan dari queue ketika consumer mengirim ACK (acknowledgement) bahwa proses sukses. Jika consumer mati tanpa ACK, pesan akan requeue (dikembalikan ke queue) dan diproses worker lain.
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class EmailWorker {
@RabbitListener(queues = "task.email")
public void handle(String message) {
// Satu pesan hanya diproses satu worker.
// Jika method ini selesai tanpa exception -> pesan di-ACK otomatis.
// Jika lempar exception -> pesan di-requeue (default) atau masuk DLQ.
emailService.send(message);
}
}
Spring Integration bahkan menyediakan adapter agar service Anda bisa mengirim dan menerima pesan RabbitMQ tanpa menulis kode transport sama sekali — cukup deklarasikan gateway yang dipetakan ke exchange/queue.
Perbedaan mental model: di Kafka konsumen menarik pesan berdasarkan offset dari log; di RabbitMQ broker mendorong pesan ke consumer dari queue. Di Kafka, dua konsumen group berbagi topik yang sama; di RabbitMQ, dua queue terpisah (masing-masing ter-binding ke exchange yang sama) yang membuat pesan terkirim ke dua penerima.
Kapan Memakai RabbitMQ
- Task queue klasik: pekerjaan seperti kirim email, resize gambar, generate PDF.
- Routing yang fleksibel: butuh wildcard, header-based routing, atau memilih queue berdasarkan aturan kompleks.
- Request-reply: pola tanya-jawab sinkron yang dijalankan lewat queue.
- Retry per pesan: penanganan gagal yang granular dengan DLQ dan scheduler.
- Arsitektur dengan pola campuran (queue + pub-sub) dalam satu broker.
Kafka vs RabbitMQ
Kedua broker sama-sama hebat tapi dirancang untuk masalah yang berbeda. Tabel berikut merangkum perbandingannya:
| Aspek | Kafka | RabbitMQ |
|---|---|---|
| Model pesan | Log persisten (topic/partition) | Queue + exchange (AMQP) |
| Delivery | Publish-subscribe dominan | Queue & publish-subscribe |
| Urutan pesan | Per partition | Per queue (jika satu consumer) |
| Replay pesan | Ya, baca ulang via reset offset | Tidak (pesan terhapus setelah ACK) |
| Throughput | Sangat tinggi (jutaan msg/s) | Sedang (puluhan ribu msg/s) |
| Routing | Tidak ada (partisi by key) | Kaya: direct, topic, fanout, headers |
| Latensi | Milidetik (jangan untuk real-time sempit) | Mikrodetik (sangat cepat) |
| Konsumen | Menarik sendiri (pull) via offset | Didorong broker (push), perlu ACK |
| Konsumen banyak | Banyak consumer group independen | Perlu banyak queue/binding |
| Retensi | Simpan sesuai retention policy | Hapus setelah konsumsi |
Aturan praktis singkat: aliran event bervolume besar yang perlu dibaca ulang → Kafka. Task yang perlu diproses sekali, dengan routing & retry canggih → RabbitMQ.
Reliabilitas & Failure Handling
Sistem event-driven yang baik harus tahan terhadap kegagalan: konsumen bisa error, database bisa down, skema bisa berubah. Bagian ini membahas mekanisme standar.
Retry dan Dead-Letter Queue (DLQ)
Ketika konsumen gagal memproses pesan, kita tidak ingin langsung menyerah — mungkin kegagalannya sementara (database sedang restart). Ada dua strategi:
- Retry langsung (immediate retry): coba lagi beberapa kali dengan jeda singkat.
- Delayed retry (backoff): taruh pesan di queue retry yang baru dibaca setelah beberapa detik/menit — jeda semakin panjang setiap percobaan (exponential backoff). Ini mencegah thundering herd: semua pesan gagal menyerbu konsumen bersamaan.
Jika setelah percobaan maksimal pesan masih gagal, pesan dipindahkan ke Dead-Letter Queue (DLQ) — queue khusus "karantina" untuk pesan yang berkali-kali gagal. DLQ tidak diproses oleh worker normal; tujuannya untuk inspeksi. Tim bisa menganalisis kenapa pesan gagal, memperbaiki bug atau data, lalu me-replay pesan secara manual.
[Queue utama] -> gagal (retry 3x, backoff) -> gagal lagi
\--------> [DLQ: order.failed] -> inspeksi manual
Idempotent Consumer
Karena pesan bisa terkirim lebih dari sekali (dan retry membuat konsumen memproses ulang), konsumen harus idempoten: memproses pesan yang sama dua kali memberi hasil yang sama seperti sekali. Contohnya: saat membuat pembayaran, simpan idempotencyKey (misalnya orderId) dan tolak permintaan kedua dengan key yang sama. Prinsip ini persis yang dijelaskan dalam artikel Stripe Payment Webhook dan dokumentasi idempotency Stripe.
// Contoh: cek sudah-diproses sebelum mengeksekusi efek samping.
if (processedOrderDao.exists(event.getOrderId())) {
log.warn("Order {} sudah diproses, abaikan duplikat.", event.getOrderId());
return;
}
// ... proses ...
processedOrderDao.markProcessed(event.getOrderId());
Mengapa idempotensi wajib? Dengan at-least-once delivery, duplikasi bukan pengecualian — ia adalah perilaku normal. Konsumen yang tidak idempoten akan mengirim email dua kali, mengurang stok dua kali, atau memproses pembayaran dua kali.
Delivery Semantics
Saat membangun sistem, pilih semantic yang sesuai trade-off antara kehilangan pesan dan duplikasi:
| Semantics | Arti | Resiko | Contoh |
|---|---|---|---|
at-most-once | Pesan diproses paling banyak sekali, boleh hilang | Pesan bisa hilang | Telemetri non-kritis, log analytics |
at-least-once | Pesan diproses minimal sekali, boleh duplikat | Duplikasi (perlu idempotency) | Pembayaran, stok, email — default umum |
exactly-once | Pesan diproses tepat sekali | Kompleks & mahal | Keuangan, di mana duplikat berbahaya |
Di Kafka, exactly-once dimungkinkan lewat transactional producer dan idempotent producer. Di RabbitMQ, kombinasi ACK + idempotent consumer yang realistis mencapai "effectively once" — secara logika sama dengan tepat-sekali, tanpa jaminan protokol.
Pola Event-Driven yang Perlu Dikenal
Dua pola arsitektur sering muncul bersama message broker:
- Event Sourcing: alih-alih hanya menyimpan state terbaru, simpan seluruh rangkaian event sebagai sumber kebenaran (source of truth). State saat ini bisa dibangun ulang kapan saja dengan memutar ulang event — sangat cocok dengan model log Kafka.
- Outbox Pattern: saat menyimpan data di database, tulis juga event ke tabel outbox dalam transaksi yang sama, lalu publisher membaca tabel outbox dan mengirim event ke broker setelah transaksi commit. Ini mencegah masalah klasik dual-write: kalau Anda mengirim event ke broker di dalam transaksi DB yang belum commit, pesan bisa terkirim padahal datanya gagal tersimpan (atau sebaliknya). Outbox menjamin event tidak hilang dan konsisten dengan data.
Service Database Broker
| 1. INSERT order (TX) |
| 2. INSERT outbox (TX) | --commit transaksi yang sama-->
| |
| 3. baca outbox |
| 4. publish event ------->| --> consumer
Jebakan & Best Practice
Pengalaman tim di lapangan memunculkan beberapa jebakan yang paling sering terjadi:
- Jangan jadikan MQ sebagai database. Message broker bukan penyimpanan jangka panjang untuk query. Kafka punya retention, tapi memindai jutaan event untuk mencari satu record adalah anti-pattern — simpan state final di database (seperti PostgreSQL), dan gunakan broker untuk aliran event. Baca Strategi Indexing PostgreSQL untuk menyimpan state yang di-query cepat.
- Perhatikan message size. Pesan besar (misalnya payload 10 MB berisi gambar) menyumbat broker dan merusak throughput. Kirim referensi (URL/ID) alih-alih payload penuh, dan simpan objek besar di object storage.
- Waspadai consumer lag. Jika konsumen lebih lambat dari producer, offset tertinggal jauh dan latensi pemrosesan membengkak. Lag = selisih offset terakhir yang ditulis dengan offset yang sudah dibaca konsumen.
- Monitor lag secara proaktif. Gunakan alat seperti Kafka Lag Monitoring atau dashboards JMX untuk mendeteksi konsumen yang mogok sebelum antrean menumpuk. Pasang alert saat lag melewati ambang batas.
- Versioning skema event. Skema event adalah kontrak antar service. Menambah field baru biasanya aman (backward compatible), tapi menghapus/merename field memecah consumer lama. Terapkan versioning — misalnya
order.created.v1vsorder.created.v2— dan hindari breaking change tanpa koordinasi. Serialization framework seperti Avro/Protobuf dengan Schema Registry membantu. - Jangan publish di dalam transaksi DB yang belum commit tanpa outbox. Menulis ke broker sebelum commit membuat event bisa bocor untuk data yang batal; menulis setelah commit membuat data mungkin gagal padahal event sudah terkirim. Gunakan outbox pattern untuk konsistensi.
- Buat consumer idempoten. Selalu asumsikan pesan bisa duplikat (lihat bagian idempotency di atas).
Studi Kasus: E-Commerce dengan order.created
Mari satukan semuanya. Sebuah platform e-commerce dengan layanan order-service, email-service, stock-service, dan notification-service. Dengan Kafka:
order-servicemenerimaPOST /orders, menyimpan order + event di tabel outbox dalam satu transaksi.- Outbox publisher membaca tabel outbox dan menulis event
order.createdke topic Kafka — sekali, dan tidak hilang. - Tiga service berlangganan topic yang sama, masing-masing dengan consumer group sendiri sehingga semuanya menerima salinan event yang sama:
email-servicemengirim email konfirmasi.stock-servicemengurangi stok (idempoten, viaorderId).notification-servicemengirim push notification ke aplikasi mobile.
- Jika
email-servicegagal mengirim (SMTP down), Spring Kafka melakukan retry dengan backoff, lalu pesan dipindahkan ke DLQ untuk diinspeksi tim. - Jika suatu saat ada bug di
stock-servicedan sudah diperbaiki, offset di-reset sehingga event lama direplay — stok dikoreksi tanpa producer mengirim ulang apa pun.
/--> consumer group "email-service"
order-service --> consumer group "stock-service"
| outbox + publish --> topic order.created
\--> consumer group "notification-service"
Arsitektur ini sama dengan yang dibahas dalam Resilient Microservices: service terisolasi, satu gagal tidak menjatuhkan yang lain, dan sistem tetap bisa menyerap lonjakan beban karena broker bertindak sebagai buffer.
Ringkasan
Message broker adalah tulang punggung arsitektur event-driven: ia memisahkan producer dari consumer, memungkinkan satu event dikonsumsi banyak service, dan menyimpan pesan sementara sebagai buffer saat beban puncak. Kafka adalah log persisten yang replayable — pilihan untuk aliran event bervolume besar dengan banyak consumer group. RabbitMQ adalah queue dengan routing kaya (exchange direct/topic/fanout/headers) — pilihan untuk task queue dan pola request-reply. Keduanya membutuhkan retry + DLQ untuk kegagalan, consumer idempoten untuk toleransi duplikasi, dan outbox pattern agar event konsisten dengan database. Pilih broker berdasarkan kebutuhan: throughput & replay → Kafka; routing & task queue → RabbitMQ. Dan jangan lupa: broker itu koridor pesan, bukan database.
Lanjut membaca
- Kafka: dokumentasi resmi — konsep dan terminology
- Confluent: pengantar platform Kafka — ekosistem Kafka secara menyeluruh
- RabbitMQ: AMQP Concepts — exchange, queue, binding
- Spring Kafka Reference —
@KafkaListener, producer, retry - Spring Integration: RabbitMQ — integrasi RabbitMQ tanpa boilerplate
- WebSocket & Real-Time — mengalirkan event ke frontend secara langsung
- Observability: Logging & Monitoring — memantau lag dan kesehatan broker
- Stripe Payment Webhook — idempotency di dunia nyata