Message Broker: Kafka & RabbitMQ

15 min readAdvanced
KafkaRabbitMQMessage BrokerEvent-Driven

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:

  1. Latensi membengkak. POST /orders menunggu email terkirim (bisa ratusan milidetik) padahal pelanggan cuma ingin order tercatat.
  2. Coupling antar service. Service order harus tahu alamat service email, stok, dan notifikasi. Menambah satu consumer baru berarti mengubah kode order.
  3. Tidak ada buffering. Kalau service email sedang down atau lambat, request order ikut gagal atau menumpuk di memori server.
  4. 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.created akan 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:

AspekPoint-to-point QueuePublish-Subscribe
ModelSatu pesan → satu consumerSatu event → semua subscriber
KonsumenBersaing (competitor) memperebutkan pesanBerlangganan (subscribe) pada topic
ContohTask queue, email worker, pembayaranEvent order.created ke banyak service
Jika consumer matiPesan antre, diproses worker lainEvent 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

![Arsitektur Kafka — publish/subscribe](/images/articles/kafka-broker.svg)
KonsepPenjelasan
ProducerAplikasi yang menulis pesan (event) ke topic
TopicKategori pesan, misalnya order.created atau user.login
PartitionUnit paralelisme dalam topic. Pesan dipecah ke beberapa partition; urutan dijamin per partition, bukan lintas partition
OffsetNomor posisi sebuah pesan di dalam partition. Konsumen menandai offset yang sudah dibaca
ConsumerAplikasi yang membaca pesan dari partition
Consumer GroupKumpulan consumer yang berbagi beban: satu partition dibaca maksimal oleh satu consumer dalam group yang sama
BrokerSatu server Kafka. Kumpulan broker disebut cluster
RetentionKebijakan 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

KonsepPenjelasan
ProducerAplikasi yang mengirim pesan ke exchange
Exchange"Pos routing": menerima pesan lalu memutuskan ke queue mana pesan diteruskan
QueueAntrean berisi pesan yang menunggu diproses consumer
Binding & Routing KeyAturan yang menghubungkan exchange ke queue; routing key adalah "alamat" yang dicocokkan binding
ConsumerAplikasi yang mengambil pesan dari queue

Kunci memahami RabbitMQ adalah exchange. Ada empat jenis exchange dengan cara routing yang berbeda:

Jenis ExchangeCara RoutingCocok untuk
directPesan dikirim ke queue yang binding-nya persis sama dengan routing keyJob spesifik, misalnya task.email
topicRouting key dicocokkan dengan pola wildcard (* satu kata, # banyak kata)Routing berjenjang, misal order.created.eu
fanoutPesan disalin ke semua queue yang ter-binding (mengabaikan routing key)Broadcast / publish-subscribe
headersCocokkan berdasarkan header pesan, bukan routing keyRouting 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:

AspekKafkaRabbitMQ
Model pesanLog persisten (topic/partition)Queue + exchange (AMQP)
DeliveryPublish-subscribe dominanQueue & publish-subscribe
Urutan pesanPer partitionPer queue (jika satu consumer)
Replay pesanYa, baca ulang via reset offsetTidak (pesan terhapus setelah ACK)
ThroughputSangat tinggi (jutaan msg/s)Sedang (puluhan ribu msg/s)
RoutingTidak ada (partisi by key)Kaya: direct, topic, fanout, headers
LatensiMilidetik (jangan untuk real-time sempit)Mikrodetik (sangat cepat)
KonsumenMenarik sendiri (pull) via offsetDidorong broker (push), perlu ACK
Konsumen banyakBanyak consumer group independenPerlu banyak queue/binding
RetensiSimpan sesuai retention policyHapus 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:

  1. Retry langsung (immediate retry): coba lagi beberapa kali dengan jeda singkat.
  2. 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:

SemanticsArtiResikoContoh
at-most-oncePesan diproses paling banyak sekali, boleh hilangPesan bisa hilangTelemetri non-kritis, log analytics
at-least-oncePesan diproses minimal sekali, boleh duplikatDuplikasi (perlu idempotency)Pembayaran, stok, email — default umum
exactly-oncePesan diproses tepat sekaliKompleks & mahalKeuangan, 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.v1 vs order.created.v2 — dan hindari breaking change tanpa koordinasi. Serialization framework seperti Avro/Protobuf dengan Schema Registry membantu.
  6. 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.
  7. 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:

  1. order-service menerima POST /orders, menyimpan order + event di tabel outbox dalam satu transaksi.
  2. Outbox publisher membaca tabel outbox dan menulis event order.created ke topic Kafka — sekali, dan tidak hilang.
  3. Tiga service berlangganan topic yang sama, masing-masing dengan consumer group sendiri sehingga semuanya menerima salinan event yang sama:
    • email-service mengirim email konfirmasi.
    • stock-service mengurangi stok (idempoten, via orderId).
    • notification-service mengirim push notification ke aplikasi mobile.
  4. Jika email-service gagal mengirim (SMTP down), Spring Kafka melakukan retry dengan backoff, lalu pesan dipindahkan ke DLQ untuk diinspeksi tim.
  5. Jika suatu saat ada bug di stock-service dan 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

← Back to technical articles