Belajar Backend Developer - Event-Driven & Streaming
Episode 22 of 28

Belajar Backend Developer - Event-Driven & Streaming

Membangun sistem yang reaktif: arsitektur event-driven dengan Kafka, event sourcing yang mencatat sejarah sebagai sumber kebenaran, dan pola CQRS yang memisahkan jalan tulis dari jalan baca

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

Pendahuluan

Di episode 10 kalian memakai queue untuk pekerjaan asinkron sederhana. Episode 22 menaikkan level: event-driven architecture dan streaming — paradigma di mana sistem bukan sekadar menyimpan keadaan terakhir, melainkan mengalirkan dan merekam peristiwa yang terjadi. Ini fondasi sistem modern: logistik, finansial, platform data, dan AI pipeline.

Mengapa penting? Karena pola "tanya database, jawab status" tidak cukup untuk sistem yang kompleks — banyak layanan butuh tahu apa yang terjadi, bukan hanya keadaan sekarang. Event-driven memungkinkan banyak konsumen bereaksi terhadap satu peristiwa, merekonstruksi keadaan dari riwayat, dan menyelaraskan sistem yang sebelumnya terpisah.

Event-Driven Architecture

Prinsipnya: komponen tidak memanggil satu sama lain secara langsung, melainkan menerbitkan event yang dikonsumsi siapa saja yang peduli.

100%

Keuntungan dibanding pemanggilan sinkron (episode 12):

  • Decoupling — penerbit tidak tahu siapa konsumennya; menambah konsumen baru tanpa mengubah penerbit.
  • Skalabilitas independen — setiap konsumen diskala sendiri.
  • Replay — event bisa dibaca ulang oleh konsumen baru.

Kafka sebagai Event Bus

Kafka (dari episode 10) adalah event bus standar. Konsep intinya: topics yang menampung event dalam partitions, dan consumer group yang melacak offset.

Menerbitkan event order.placed
import { Kafka } from "kafkajs"
 
const kafka = new Kafka({ brokers: process.env.KAFKA_BROKERS.split(",") })
const producer = kafka.producer()
 
export async function publishOrderPlaced(order: Order) {
  await producer.send({
    topic: "order-events",
    messages: [{
      // key menentukan partition → urutan per order terjaga
      key: String(order.id),
      value: JSON.stringify({
        type: "order.placed",
        orderId: order.id,
        userId: order.userId,
        items: order.items,
        total: order.total,
        at: new Date().toISOString(),
      }),
    }],
  })
}

Key ke Partition

Event dengan key yang sama (misal orderId) selalu masuk partition yang sama — sehingga urutan event per order terjaga. Ini penting: order.cancelled tidak boleh diproses sebelum order.placed.

Event vs Command

  • Event — fakta yang sudah terjadi: order.placed, payment.succeeded. Konsumen tidak bisa "menolak".
  • Command — permintaan untuk melakukan sesuatu: "kirim email". Bisa ditolak.

Pisahkan keduanya: event untuk menyebarkan fakta, command/queue untuk tugas yang harus dikerjakan.

Event Sourcing

Event sourcing mengubah cara data disimpan: alih-alih hanya menyimpan keadaan terakhir, simpan seluruh riwayat event sebagai sumber kebenaran. Keadaan saat ini hanyalah proyeksi dari riwayat.

Tanpa event sourcing

text
order 87 → { status: "shipped", total: 300000 }   # keadaan terakhir saja

Dengan event sourcing

text
order 87:
  1. order.placed     → total 300000
  2. payment.succeeded
  3. order.shipped
  4. order.delivered

Keuntungan: sejarah penuh — audit (episode 19), analisis, dan time travel (rekonstruksi keadaan di masa lalu) tersedia gratis. Setiap event tidak bisa diubah (immutable).

Rekonstruksi state dari event stream
export function projectOrder(events: OrderEvent[]): OrderState {
  return events.reduce<OrderState>((state, e) => {
    switch (e.type) {
      case "order.placed":
        return { ...state, id: e.orderId, status: "pending", total: e.total }
      case "payment.succeeded":
        return { ...state, status: "paid" }
      case "order.shipped":
        return { ...state, status: "shipped" }
      case "order.cancelled":
        return { ...state, status: "cancelled" }
      default:
        return state
    }
  }, { status: "unknown" })
}

Trade-off event sourcing: kompleksitas penyimpanan (event store), dan state saat ini baru bisa dibaca setelah projection berjalan. Karena itu event sourcing hampir selalu dipasangkan dengan CQRS.

Note

Event sourcing tidak menggantikan database biasa. Ada dua gudang: event store (sumber kebenaran, append-only) dan read model (database normal berisi state terproyeksi untuk dibaca cepat). Jangan memaksa semua data jadi event — mulai dari domain yang butuh sejarah & audit (order, payment), bukan yang sederhana.

CQRS: Command Query Responsibility Segregation

CQRS memisahkan jalan tulis dari jalan baca:

  • Command side — menerima perubahan (create/update) → menyimpan event.
  • Query side — membaca data via read model yang dioptimalkan untuk query.
100%

Mengapa terpisah? Karena kebutuhan tulis dan baca sering bertolak belakang. Tulis butuh konsistensi dan validasi ketat; baca butuh kecepatan dan bentuk yang nyaman. Read model bisa berbentuk persis yang dibutuhkan UI — pre-joined, pre-aggregated, tanpa join mahal.

Proyeksi

Read model diperbarui oleh projector yang mengonsumsi event:

Projector - memperbarui read model
consumer.subscribe({ topic: "order-events" })
 
await consumer.run({
  eachMessage: async ({ message }) => {
    const event = JSON.parse(message.value.toString())
 
    if (event.type === "order.placed") {
      await db.query(
        `INSERT INTO order_read (id, user_id, status, total)
         VALUES ($1, $2, 'pending', $3)
         ON CONFLICT (id) DO UPDATE SET status = EXCLUDED.status`,
        [event.orderId, event.userId, event.total],
      )
    }
  },
})

Kapan Memakai (dan Kapan Tidak)

Cocok untuk event-driven + event sourcing

  • Domain dengan banyak konsumen — satu peristiwa memicu banyak reaksi (order → email, stok, invoice, analytics).
  • Butuh audit penuh — finansial, compliance, sistem yang harus bisa menjelaskan sejarah.
  • Multiple services perlu data yang sama — event sebagai kontrak antar service.

Tidak cocok

  • CRUD sederhana — blog, katalog tanpa kebutuhan sejarah → query biasa sudah cukup.
  • Butuh konsistensi transaksional ketat (episode 5) — event-driven adalah eventual consistency, bukan ACID.
  • Tim kecil tanpa pengalaman — event sourcing menambah kompleksitas nyata.

Warning

Event-driven berarti eventual consistency: setelah publish, read model bisa tertinggal beberapa saat. Endpoint "baca order" mungkin belum mencerminkan event yang baru saja terjadi. Untuk domain yang wajib baca-segera-setelah-tulis (misal konfirmasi checkout), pertimbangkan kombinasi tulis langsung + event, atau komunikasikan keterlambatan ini dengan jelas.

Praktik: Event Pipeline Toko Online

Terapkan alur lengkap:

  1. Order Service menerima POST /v1/orders → simpan event order.placed ke Kafka.
  2. Projector memperbarui read model (tabel order_read).
  3. Email consumer (episode 10) mengirim konfirmasi.
  4. Inventory consumer mengurangi stok.
  5. Analytics consumer menghitung metrik — tanpa mengubah Order Service.
Pantau topic Kafka
docker exec kafka-lab kafka-topics.sh --list --bootstrap-server localhost:9092
docker exec kafka-lab kafka-console-consumer.sh \
  --topic order-events --from-beginning --bootstrap-server localhost:9092

Konsumen baru bisa dibaca dari awal (from-beginning) — inilah daya replay event-driven.

Common Pitfalls

Event Berisi Perintah

Menerbitkan "to do X" sebagai event mengikat semua konsumen pada interpretasi yang sama. Event = fakta (order.placed); tindakan = keputusan konsumen.

Event Tanpa Schema

Event yang berubah bentuk merusak semua konsumen diam-diam. Schema Registry (AVRO/JSON Schema) + contract testing (episode 8) wajib untuk event.

Memaksa Eventual Consistency pada Domain ACID

Transfer saldo pakai event-driven → saldo bisa salah baca. Uang tetap butuh transaction (episode 5).

Event Sourcing untuk Semua

CRUD sederhana jadi ribet tanpa manfaat. Pilih domain yang butuh sejarah.

Penutup

Episode 22 membangun sistem reaktif: arsitektur event-driven dengan Kafka, event sourcing yang menjadikan riwayat sebagai sumber kebenaran, dan CQRS yang memisahkan jalan tulis dan baca.

Inti yang harus dibawa pulang:

  • Event-driven = publish fakta, siapa pun boleh konsumsi; decoupled & scalable.
  • Event immutable dan bisa replay; key ke partition menjaga urutan.
  • Event sourcing: riwayat adalah kebenaran; state adalah proyeksi.
  • CQRS: command side menyimpan, query side membaca read model.
  • Eventual consistency ≠ ACID; pilih pola sesuai domain.

Di episode 23 selanjutnya kita akan membawa backend ke era 2026: AI & LLM integration — mengintegrasikan LLM API, membangun RAG backend, dan orchestration agent. Sampai jumpa di episode 23!

Belajar Backend Developer - Event-Driven & Streaming | Belajar Backend