Menguasai komunikasi asinkron secara utuh: perbedaan event, command, dan message; queue vs stream dan konsep inti Kafka; delivery semantics beserta idempotent consumer; outbox pattern; hingga event sourcing dan CQRS — dengan rancangan topik event studi kasus e-commerce

Setelah di episode 12 kalian belajar mengekstraksi microservices — boundary per domain, saga orchestration, dan failure handling antar service — pada episode ini kita perdalam tulang punggung komunikasinya: event-driven architecture (EDA).
EDA sudah kita sentuh bertahap: in-process event bus di monolith (episode 3), integrasi data via event + outbox (episode 8), dan saga choreography (episode 12). Kini saatnya menyatukan semuanya menjadi disiplin lengkap: apa bedanya event dan command, bagaimana Kafka bekerja di balik abstraksinya, dan kapan event sourcing benar-benar layak.
Tiga istilah yang sering tertukar padahal semantiknya berbeda:
| Jenis | Arah | Sifat | Contoh |
|---|---|---|---|
| Command | 1 penerima eksplisit | Imperatif, bisa ditolak | ReserveStock(orderId) |
| Event | 0..N subscriber | Fakta masa lampau, tak bisa ditolak | StockReserved(orderId) |
| Message | Payung umum | Apa pun yang dikirim via broker | Keduanya |
Pembeda paling penting: command adalah niat ("lakukan ini"), event adalah sejarah ("ini sudah terjadi"). Konsekuensi desainnya besar: publisher event tidak boleh peduli siapa mengonsumsinya; sedangkan pengirim command tahu persis targetnya. Menyebut event dengan kata kerja imperatif (SendEmailEvent) adalah bau desain — itu command yang menyamar.
Dua keluarga broker, dua model mental:
Queue (RabbitMQ/SQS) : pesan dihapus setelah dikonsumsi & ack
routing cerdas, DLQ mudah, cocok task/job
Stream (Kafka) : log append-only, pesan tetap tersimpan
replayable, ordered per partition,
cocok aliran fakta & integrasi sistemKafka layak dipahami architect karena ia standar de facto backbone event:
orderId) mendarat di partition sama → urutan pemrosesan terjamin per entitas. Ini jawaban atas "bagaimana menjamin order diproses berurutan".Konsekuensi arsitekturalnya dalam: stream yang replayable mengubah cara kita memikirkan bug dan audit — konsumen baru bisa membangun ulang state dari awal log tanpa snapshot database.
Broker menjanjikan salah satu dari tiga jaminan:
At-most-once : fire & forget, bisa hilang
At-least-once : default Kafka, tidak hilang tapi bisa dobel
Effectively-once: at-least-once + idempotency end-to-endInsight praktis yang jarang ditekankan: exactly-once end-to-end nyaris mustahil dan biasanya tidak perlu. Yang dibangun industri adalah at-least-once delivery + idempotent consumer — konsumen aman memproses event yang datang dua kali.
Teknik idempotency di consumer:
eventId unik; duplikat dilewati (dalam transaksi yang sama dengan mutasi datanya).PaymentReceived pada order status PAID adalah no-op, bukan error.Plus dua perlengkapan wajib produksi: dead letter queue untuk event yang gagal diproses berkali-kali (jangan biarkan poison message memblokir partisi), dan monitoring lag — consumer lag tinggi berarti pipeline tertinggal dari realitas.
Sudah diperkenalkan di episode 8; kini lihat posisinya dalam EDA. Masalah dual-write:
1. COMMIT database ✓
2. PUBLISH ke broker ✗ (crash)
→ sistem kehilangan event selamanya
atau sebaliknya:
1. PUBLISH ✓, 2. COMMIT ✗
→ event bohong tentang fakta yang tak pernah adaOutbox menyelesaikannya dengan satu transaksi lokal: data bisnis + event masuk tabel outbox bersamaan, relay proses menerbitkan ke broker lalu menandai terkirim. Consumer tetap harus idempotent (relay bisa terbit dua kali), tapi fakta dan pengumumannya tak lagi bisa terpisah.
Alternatif yang perlu kalian kenal: change data capture (CDC) via Debezium membaca transaction log database langsung menjadi event — tanpa tabel outbox, tapi mengikat event ke bentuk skema database.
Mari desain topologi event e-commerce kita:
topics:
- name: ordering.orders
key: orderId
events: [OrderCreated, OrderCancelled]
retention: 30d
consumers: [analytics, shipping, cs-dashboard]
- name: inventory.stock
key: sku
events: [StockReserved, StockReleased, StockChanged]
retention: 30d
consumers: [ordering, catalog-badges]
- name: payment.payments
key: orderId
events: [PaymentReceived, PaymentFailed, RefundIssued]
retention: 90d # jalur uang: retensi lebih panjang
consumers: [ordering, finance-reporting]
rules:
schema_registry: avro, compatibility=BACKWARD
naming: "<domain>.<aggregate>"
payload: hanya ID + data minimal; detail via API pemilik
guarantees:
producer: via outbox table
consumer: processed_event table + state machine guard
dlq: per-consumer-group, alert saat depth > 0Tiga keputusan desain di dalamnya layak digarisbawahi:
Important
Schema evolution adalah penyebab insiden EDA nomor satu. Tanpa registry dan aturan compat, perubahan field oleh tim payment akan meruntuhkan consumer finance diam-diam. Perlakukan event schema seperti API publik: contract test dan review wajib.
Event sourcing melangkah lebih jauh: state tidak disimpan sebagai snapshot mutable, melainkan direkonstruksi dari rangkaian event. Order bukan baris orders yang di-update; ia adalah aliran OrderCreated → ItemAdded → Paid → Shipped yang di-fold menjadi state kini.
Keunggulan nyata:
Harga yang harus dibayar juga nyata:
SELECT * WHERE.Heuristik keputusan: pakai event sourcing hanya untuk agregat yang audit trail-nya bernilai bisnis (payment ledger, order lifecycle, wallet balance), bukan seluruh sistem. Catalog produk dengan event sourcing adalah kesengsaraan yang tidak diperintahkan siapa pun.
SendEmailRequested yang dipublish lalu "ditunggu" hasilnya; async dipaksa sinkron lewat belakang. Kalau butuh jawaban, panggil sinkron.Inti yang harus dibawa pulang:
Di episode 14 selanjutnya kita naik ke lapisan infrastruktur: cloud-native architecture — prinsip 12-factor app, container & Kubernetes dari mata architect, dan trade-off serverless versus container untuk workload studi kasus kita. Sampai jumpa!