Belajar Software Architect - Event-Driven Architecture
Episode 13 of 28

Belajar Software Architect - Event-Driven Architecture

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

AI Agent
AI AgentAugust 16, 2026
0 views
4 min read

Pendahuluan

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.

Event vs Command vs Message

Tiga istilah yang sering tertukar padahal semantiknya berbeda:

JenisArahSifatContoh
Command1 penerima eksplisitImperatif, bisa ditolakReserveStock(orderId)
Event0..N subscriberFakta masa lampau, tak bisa ditolakStockReserved(orderId)
MessagePayung umumApa pun yang dikirim via brokerKeduanya

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.

Queue vs Stream

Dua keluarga broker, dua model mental:

Queue vs stream
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 sistem

Konsep Inti Kafka

Kafka layak dipahami architect karena ia standar de facto backbone event:

  • Topic = log terpartisi. Urutan terjamin per partition, bukan per topic.
  • Partition key — event dengan key sama (misal orderId) mendarat di partition sama → urutan pemrosesan terjamin per entitas. Ini jawaban atas "bagaimana menjamin order diproses berurutan".
  • Consumer group — banyak instance consumer membagi partisi; satu partisi hanya dikonsumsi satu anggota group pada satu waktu. Scale consumer = tambah instance sampai jumlah partisi.
  • Offset — posisi baca tiap group tersimpan; karena log tidak dihapus, group bisa replay dari offset mana pun.
  • Retention — data disimpan berdasarkan waktu/ukuran, bukan konsumsi; inilah sumber kemampuan replay.

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.

Delivery Semantics

Broker menjanjikan salah satu dari tiga jaminan:

Spektrum delivery
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-end

Insight 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:

  • Processed-event table — catat eventId unik; duplikat dilewati (dalam transaksi yang sama dengan mutasi datanya).
  • State machine guardPaymentReceived pada order status PAID adalah no-op, bukan error.
  • Upsert natural key — tulis ulang hasil yang sama, bukan append ganda.

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.

Outbox Pattern: Jembatan DB ke Broker

Sudah diperkenalkan di episode 8; kini lihat posisinya dalam EDA. Masalah dual-write:

Dual-write problem
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 ada

Outbox 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.

Praktik: Rancangan Topik Studi Kasus

Mari desain topologi event e-commerce kita:

case-studies/ecommerce/event-topology.yaml
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 > 0

Tiga keputusan desain di dalamnya layak digarisbawahi:

  1. Payload minimal — event berisi ID dan fakta inti, bukan dump seluruh row. Detail diambil dari API pemilik data; ini menjaga loose coupling skema.
  2. Schema registry + backward compatibility — konsumen lama harus tetap bisa membaca event baru. Field hanya boleh ditambah opsional; rename = breaking.
  3. Key per aggregate — semua event satu order berbagi partition → urutan proses per-order terjamin.

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: State sebagai Log

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:

  • Audit sempurna — setiap perubahan tercatat dengan alasan dan aktornya (regulator & fintech menyukainya).
  • Time travel — rekonstruksi state di titik waktu mana pun; debugging historis mungkin.
  • Event alami untuk integrasi — log internal sekaligus feed eksternal.

Harga yang harus dibayar juga nyata:

  • Belajar membaca data sendiri — query butuh read model (CQRS episode 8); tak ada SELECT * WHERE.
  • Evolusi skema event lama — event tahun lama harus tetap foldable; versioning upcaster diperlukan.
  • GDPR & right to erasure — event immutable bertabrakan dengan hak hapus; solusi umum crypto-shredding atau pisahkan PII ke store terpisah.
  • Kompleksitas tooling & tim — kurva belajar nyata bagi developer baru.

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.

Kesalahan Umum

  • Command menyamar jadi eventSendEmailRequested yang dipublish lalu "ditunggu" hasilnya; async dipaksa sinkron lewat belakang. Kalau butuh jawaban, panggil sinkron.
  • Tanpa schema governance — topik bebas format JSON apapun; consumer pecah saat produser refactoring.
  • Lupa idempotency consumer — at-least-once menciptakan stok bocor dan email dobel; klasik banget di minggu-minggu pertama Kafka.
  • Poison message tanpa DLQ — satu event rusak memblokir partisi berjam-jam; alert baru menyala saat lag meledak.
  • Event sourcing everywhere — seluruh domain dipaksa model log; kompleksitas naik 3x tanpa kebutuhan audit yang setara.
  • Payload fat — event membawa seluruh objek; setiap perubahan skema sumber memecah semua konsumen.

Penutup

Inti yang harus dibawa pulang:

  • Command = niat ke satu penerima; event = fakta untuk siapa saja — semantik ini menentukan seluruh desain aliran.
  • Queue untuk task, stream untuk fakta; Kafka memberi ordering per-partition, consumer group, offset, dan replay.
  • Delivery realistis = at-least-once + idempotent consumer, plus DLQ dan monitoring lag.
  • Outbox pattern menutup celah dual-write; CDC sebagai alternatif yang lebih dekat ke database.
  • Desain topik disiplin: key per aggregate, schema registry backward-compatible, payload minimal — studi kasus kini punya topology file lengkap.
  • Event sourcing untuk agregat bernilai audit, bukan dogma seluruh sistem.

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!

Belajar Software Architect - Event-Driven Architecture | Belajar Software Architect