# Membangun Event-Driven Architecture Skala Besar dengan Kafka

> Diterbitkan oleh Literally Web ID pada 21 Oct 2026
> Sumber: https://literally.web.id/articles/membangun-event-driven-architecture-skala-besar-dengan-kafka

---

## 1. Titik Awal Masalah: Mengapa Arsitektur Lama Sering Ambruk di Produksi

Ingat malam jumat tiga tahun lalu ketika Flash Sale dimulai dan dashboard pemantauan infrastruktur tiba-tiba berubah merah darah? Kami baru saja meluncurkan fitur diskon besar-besaran, dan dalam hitungan detik, 50.000 pengguna bersamaan melakukan checkout. Backend PHP-FPM di belakang Nginx langsung megap-megap. Database MySQL kami—yang biasanya adem ayem dengan CPU utilitas 15%—mendadak melonjak ke 100%. Koneksi database habis terkuras, antrean *connection pool* jebol, dan dalam semenit kemudian, seluruh website *down*. Bukan karena kodenya jelek, tapi karena kami merancang sistem dengan komunikasi *synchronous* (REST API langsung ke DB) untuk proses yang seharusnya berjalan *asynchronous*.

Di dunia web modern, mengandalkan pemanggilan HTTP langsung antar layanan untuk setiap aksi seperti mencatat log transaksi, mengirim email konfirmasi, memperbarui cache Redis, dan memotong stok gudang adalah resep bencana. Ketika salah satu layanan hilir (downstream service) melambat, efek dominonya langsung merembet ke hulu, mengunci thread web server, dan memutus akses pengguna.

> [!WARNING]
> Jangan pernah menggunakan komunikasi sinkron untuk proses bisnis yang tidak membutuhkan respons instan. Mengirim email atau mencatat audit trail lewat HTTP POST blocking di tengah request checkout adalah dosa besar arsitektural.

Di sinilah Event-Driven Architecture (EDA) hadir bukan sekadar sebagai buzzword keren, tapi sebagai penyelamat nyawa. Alih-alih membuat layanan saling menelepon secara langsung, kita memutus rantai ketergantungan itu dengan menaruh sebuah sistem *message broker* di tengah. Dan ketika berbicara tentang throughput tinggi, ketahanan data, dan replikasi masif, Apache Kafka adalah rajanya. Namun, memasang Kafka tanpa memahami bagaimana partisi, consumer group, dan manajemen offset bekerja justru akan membawa Anda dari satu jenis neraka ke neraka operasional yang lain. Mari kita bedah bagaimana cara menjinakkannya.

## 2. Di Balik Layar: Bagaimana Sistem Ini Bekerja Tanpa Sihir

Banyak orang menganggap Kafka seperti antrean RabbitMQ biasa, padahal secara fundamental keduanya berbeda jauh. RabbitMQ adalah *message queue* tradisional yang menghapus pesan begitu pesan tersebut dibaca oleh consumer. Sebaliknya, Kafka adalah *distributed commit log*. Bayangkan Kafka sebagai buku besar (ledger) akuntansi raksasa yang tidak boleh dihapus sembarangan. Setiap event dicatat secara berurutan (*append-only*), disimpan di disk NVMe yang super cepat, dan dibaca oleh siapa saja sesuai kebutuhan mereka.

Mari kita ambil analogi sistem logistik gudang e-commerce. Sebuah truk kontainer datang membawa ribuan paket (events). Paket-paket ini tidak dilempar begitu saja ke satu lorong sempit. Mereka dipilah berdasarkan kode wilayah (Partisi Kafka). Di dalam setiap wilayah, ada beberapa kurir yang tergabung dalam satu tim (Consumer Group). Kafka secara cerdas memastikan bahwa satu paket di wilayah tertentu hanya diantar oleh satu kurir dalam tim tersebut, sehingga tidak ada pengiriman ganda.

```
[ Producer / Go Backend ] 
       │ (Publish JSON Event)
       ▼
┌──────────────────────────────────────────────┐
│               Kafka Cluster                  │
│  Topic: orders-created                       │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────┐ │
│  │ Partition 0 │ │ Partition 1 │ │ Part 2  │ │
│  └──────┬──────┘ └──────┬──────┘ └────┬────┘ │
└─────────┼───────────────┼─────────────┼──────┘
          │               │             │
          ▼               ▼             ▼
┌──────────────────────────────────────────────┐
│         Consumer Group: inventory-svc        │
│    [Worker 1]     [Worker 2]    [Worker 3]   │
└──────────────────────────────────────────────┘
```

Keindahan dari arsitektur ini adalah dekopling total. Layanan pembuat event (Producer, misalnya backend Node.js atau Go) tidak peduli siapa yang akan membaca data tersebut, berapa banyak layanan hilir yang membutuhkan, atau apakah layanan pembaca (Consumer) sedang mati lampu. Data aman tersimpan di Kafka selama masa retensi yang ditentukan. Dari sini, mari kita lihat bagaimana kita memilih strategi yang tepat di antara berbagai opsi yang ada.

## 3. Pilihan Pendekatan & Kompromi yang Harus Diambil

Saat merancang pipeline Kafka untuk skala jutaan event per detik, Anda akan dihadapkan pada persimpangan jalan arsitektural yang menuntut kompromi brutal antara *latency*, *throughput*, dan *durability*. 

Pertama, mari bicara soal konfigurasi `acks` (acknowledgements) pada Producer:
- **`acks=0`**: Producer mengirim data dan langsung melupakan tanpa menunggu konfirmasi broker. Sangat cepat, tapi jika broker ambruk sedetik kemudian, data lenyap selamanya. Cocok untuk metrik klik iklan (analytics non-kritis).
- **`acks=1`**: Leader partition menerima dan menulis data ke disk lokalnya, lalu langsung memberi balasan sukses. Cukup cepat, tapi rawan hilang jika leader mati sebelum sempat mereplikasi ke follower.
- **`acks=all` (atau `-1`)**: Standar emas untuk sistem finansial dan e-commerce. Data baru dikonfirmasi sukses setelah seluruh *In-Sync Replicas (ISR)* menulisnya ke disk. Konsekuensinya? Latency jaringan sedikit meningkat, tapi keamanan data terjamin mutlak.

Tabel komparasi cepat untuk membantu Anda memutuskan:

| Pendekatan / Parameter | `acks=0` (Fire & Forget) | `acks=1` (Leader Only) | `acks=all` (Full ISR Sync) | Rekomendasi Kasus |
| :--- | :--- | :--- | :--- | :--- |
| **Throughput** | Maksimal (Sangat Tinggi) | Tinggi | Menengah - Tinggi | Tergantung sensitivitas data |
| **Risiko Kehilangan Data**| Tinggi | Sedang | Mendekati Nol | Finansial wajib `all` |
| **Network Latency** | Minimal | Rendah | Sedikit lebih tinggi | Sesuaikan SLA layanan |

> [!TIP]
> Jangan pernah berkompromi pada `acks=all` untuk transaksi finansial atau inventaris barang. Lebih baik sedikit melambat di level producer daripada pusing merekonsiliasi data yang hilang di database keesokan harinya.

Dengan fondasi keputusan yang matang ini, saatnya kita menyiapkan tangan kotor untuk meracik infrastruktur di lingkungan nyata.

## 4. Meracik Lingkungan: Persiapan & Prasyarat Lapangan

Sebelum kita menulis baris kode pertama, kita harus memastikan fondasi infrastruktur kita cukup kuat menahan beban. Di produksi, menjalankan Kafka secara mandiri tanpa manajemen orkestrasi adalah tiket express menuju kegagalan operasional. Kita akan menggunakan Docker Compose untuk pengembangan lokal yang mencerminkan lingkungan produksi, didukung oleh ZooKeeper (atau KRaft untuk versi terbaru, tapi mari gunakan standar industri stabil saat ini dengan KRaft mode agar tidak pusing mengurus ZooKeeper).

Berikut adalah file `docker-compose.yml` siap produksi yang dioptimalkan untuk pengujian lokal:

```yaml
version: '3.8'

services:
  kafka:
    image: confluentinc/cp-kafka:7.5.0
    container_name: kafka-core
    ports:
      - "9092:9092"
    environment:
      KAFKA_NODE_ID: 1
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT'
      KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092'
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
      KAFKA_PROCESS_ROLES: 'broker,controller'
      KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka:29093'
      KAFKA_LISTENERS: 'PLAINTEXT://0.0.0.0:29092,CONTROLLER://0.0.0.0:29093,PLAINTEXT_HOST://0.0.0.0:9092'
      KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT'
      KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
      KAFKA_LOG_DIRS: '/var/lib/kafka/data'
      CLUSTER_ID: 'Mk3OEYBSD3T24T90eRMGrw'
    volumes:
      - kafka_data:/var/lib/kafka/data

volumes:
  kafka_data:
    driver: local
```

Jalankan dengan perintah standar:
```bash
docker compose up -d
```

Setelah container menyala, pastikan port `9092` terbuka dan siap menerima koneksi dari aplikasi backend Anda (baik itu Go, Node.js, maupun PHP worker yang berjalan di balik Supervisor). Dari sini, mari kita wujudkan implementasi nyata kodenya.

## 5. Langkah Implementasi: Membangun Solusi yang Tangguh

Mari kita bangun sebuah skenario nyata: Layanan Checkout E-commerce berbasis Go (Golang) yang mempublikasikan event `OrderCreated`, dan sebuah Consumer Service yang memproses pembayaran serta mengelola inventaris secara asinkron.

Kita mulai dengan Producer dalam Go:

```go
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"time"

	"github.com/confluentinc/confluent-kafka-go/v2/kafka"
)

type OrderEvent struct {
	OrderID   string    `json:"order_id"`
	UserID    string    `json:"user_id"`
	Amount    float64   `json:"amount"`
	Timestamp time.Time `json:"timestamp"`
}

func main() {
	p, err := kafka.NewProducer(&kafka.ConfigMap{
		"bootstrap.servers": "localhost:9092",
		"acks":              "all", // Jaminan durabilitas maksimal
		"retries":           5,
		"max.in.flight.requests.per.connection": 1, // Menjaga urutan pesan
	})
	if err != nil {
		log.Fatalf("Gagal membuat Kafka producer: %s", err)
	}
	defer p.Close()

	topic := "orders-created"
	order := OrderEvent{
		OrderID:   "ORD-998234",
		UserID:    "USR-55412",
		Amount:    150000.00,
		Timestamp: time.Now(),
	}

	payload, err := json.Marshal(order)
	if err != nil {
		log.Fatalf("Gagal serialisasi JSON: %s", err)
	}

	// Kirim event secara asinkron
	err = p.Produce(&kafka.Message{
		TopicPartition: kafka.TopicPartition{Topic: &topic, Partition: kafka.PartitionAny},
		Value:          payload,
		Key:            []byte(order.OrderID), // Key yang sama menjamin masuk ke partisi yang sama
	}, nil)

	if err != nil {
		log.Fatalf("Gagal mengirim pesan: %s", err)
	}

	// Tunggu sampai pengiriman selesai terkonfirmasi
	p.Flush(15 * 1000)
	fmt.Println("Event OrderCreated berhasil dikirim ke Kafka!")
}
```

> [!IMPORTANT]
> Penggunaan `Key` pada `kafka.Message` sangat krusial. Dengan memberikan `order.OrderID` sebagai key, Kafka akan menggunakan algoritma hashing untuk memastikan semua event dengan ID order yang sama selalu masuk ke partisi yang persis sama, menjaga urutan kronologis kejadian.

Namun, mengirim data saja baru separuh perjalanan. Tantangan sebenarnya justru terletak pada bagaimana Consumer membaca data tersebut tanpa kehilangan akal (dan tanpa kehilangan data akibat crash mendadak).

## 6. Membedah Baris Kode Krusial yang Kerap Menjebak

Bagian paling rawan dalam membangun Event-Driven Architecture bukan saat menulis ke Kafka, melainkan bagaimana Consumer mengelola **Offset** dan menangani kegagalan melalui **Dead Letter Queue (DLQ)** pattern.

Jika consumer membaca pesan, mulai memprosesnya ke database MySQL, lalu aplikasi crash *sebelum* offset sempat di-commit ke Kafka, apa yang terjadi? Saat restart, consumer akan membaca pesan yang sama lagi. Jika kode Anda tidak **idempoten**, pengguna bisa ditagih dua kali atau stok barang terpotong dua kali!

Mari lihat implementasi Consumer Go yang aman dengan manajemen offset manual dan penanganan DLQ:

```go
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"

	"github.com/confluentinc/confluent-kafka-go/v2/kafka"
)

func main() {
	c, err := kafka.NewConsumer(&kafka.ConfigMap{
		"bootstrap.servers":     "localhost:9092",
		"group.id":              "inventory-service-group",
		"auto.offset.reset":     "earliest",
		"enable.auto.commit":    false, // KUNCI: Kita commit manual demi keamanan!
	})
	if err != nil {
		log.Fatalf("Gagal membuat consumer: %s", err)
	}
	defer c.Close()

	topics := []string{"orders-created\