# Membangun Event-Driven Architecture Tangguh dengan Kafka dan Go

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

---

## 1. Alarm Tengah Malam: Saat Layanan Produksi Mulai Tumbang

Pukul 02:34 WIB, ketenangan malam mendadak pecah saat ponsel saya bergetar liar dengan nada alarm PagerDuty bernada tinggi. Di layar, metrik Grafana menyala merah terang: latensi API gateway melonjak dari rata-rata 45 milidetik menjadi lebih dari 8.500 milidetik, sementara error rate HTTP 504 Gateway Timeout merangkak naik menyentuh angka 38%. Sebagai Principal Systems Architect yang baru saja memejamkan mata dua jam setelah migrasi besar, adrenalin langsung mengalir deras. Kami baru saja meluncurkan arsitektur event-driven baru menggunakan Apache Kafka sebagai pusat saraf logistik dan sekumpulan microservices Go sebagai konsumernya. Bayangan tentang sistem yang gesit, terdesentralisasi, dan tahan banting seketika runtuh di hadapan kenyataan pahit: pipa data kami mengalami choke total.

Masalah bermula ketika lonjakan traffic flash sale tengah malam menghantam sistem web berbasis Nginx dan PHP-FPM di front-end, yang kemudian melemparkan event pembelian ke topik Kafka berkapasitas tinggi. Di seberang sana, pool worker Go yang bertugas memproses event tersebut mendadak terdiam. Pesan-pesan menumpuk di dalam antrean broker, menghasilkan lag yang mengular hingga jutaan offset. Nginx mulai mengembalikan error karena backend Go kehabisan koneksi database MySQL, terjebak dalam siklus deadlock dan memori yang membengkak akibat goroutine leak. Ini bukan sekadar bug biasa, melainkan kegagalan desain fundamental dalam cara kami mengelola partisi Kafka dan perilaku konsumer Go di lapangan.

> [!WARNING]
> Mengandalkan default configuration pada klien Kafka dan konsumer Go dalam skenario beban tinggi adalah resep bunuh diri infrastruktur. Tanpa manajemen memori dan strategi pembagian partisi yang presisi, sistem Anda hanya menunggu waktu untuk meledak.

Menghadapi situasi genting ini, panik bukanlah opsi. Sebagai teknisi senior, langkah pertama adalah membuka terminal, menyambungkan VPN, dan menyelam langsung ke dalam log sistem untuk mencari benang merah dari kekacauan ini, yang mengantar saya pada investigasi digital yang menegangkan.

## 2. Jejak Digital: Menelusuri Anomali Log & Lonjakan Metrik

Begitu masuk ke dalam bastions server, saya langsung membuka dashboard terminal dan meluncurkan serangkaian perintah penyelidikan untuk mendiagnosis kesehatan cluster Kafka dan container Go. Perintah pertama yang saya jalankan adalah memantau status consumer group menggunakan CLI bawaan Kafka untuk melihat seberapa parah ketimpangan pembagian beban antar partisi.

```bash
# Memeriksa consumer lag dan distribusi offset pada group 'order-fulfillment-group'
/opt/kafka/bin/kafka-consumer-groups.sh \
  --bootstrap-server kafka-broker-01.internal:9092 \
  --describe --group order-fulfillment-group
```

Hasil eksekusi terminal tersebut langsung memperlihatkan akar kejanggalan awal: dari total 32 partisi yang tersedia di topik `user-orders`, ternyata hanya ada 2 instance container Go yang aktif melayani konsumsi data. Lebih parahnya lagi, seluruh 32 partisi tersebut mengerucut dan menumpuk pada satu consumer instance saja karena konfigurasi `ClientID` dan `GroupInstanceID` yang tidak terisolasi dengan benar, sementara instance kedua dalam keadaan idle menunggu sinyal rebalance yang tak kunjung selesai.

```text
GROUP                    TOPIC          PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG             CONSUMER-ID     HOST            CLIENT-ID
order-fulfillment-group  user-orders    0          1245032         1450221         205189          go-client-xyz   10.0.4.12       go-kafka-v1
order-fulfillment-group  user-orders    1          1203911         1449830         245919          go-client-xyz   10.0.4.12       go-kafka-v1
... (semua 32 partisi dipegang oleh consumer-id yang sama)
```

Tak berhenti sampai di situ, saya juga memeriksa log aplikasi Go untuk melihat apa yang dikerjakan oleh para worker di dalam pod tersebut. Menggunakan `kubectl logs`, saya menemukan barisan error yang berulang kali muncul:

```go
// Potongan log dari aplikasi Go yang menunjukkan kegagalan commit offset akibat timeout
2023-10-24T02:35:12Z WRN [kafka-consumer] Commit offset failed: sarama: client/consumer/group session timeout expired, triggering rebalance
2023-10-24T02:35:14Z ERR [database] SQL error: Error 1205: Lock wait timeout exceeded; try restarting transaction
2023-10-24T02:35:14Z PANIC [worker] Goroutine stack trace: fatal error: out of memory
```

Pola kegagalan ini mulai membentuk sebuah cerita yang utuh. Beban pemrosesan database MySQL yang tinggi menyebabkan query INSERT dan UPDATE melambat drastis. Akibatnya, durasi pemrosesan satu batch pesan melampaui batas `max.poll.interval.ms` yang diizinkan oleh Kafka. Broker Kafka mengira konsumer tersebut telah mati (dead), memicu proses *rebalance* yang masif, yang justru semakin memperberat beban jaringan dan memori aplikasi Go hingga memicu *Out Of Memory (OOM) Panic*. Penemuan ini membawa kita langsung ke inti permasalahan arsitektural yang sebenarnya.

## 3. Membongkar Biang Kerok: Akar Masalah yang Sebenarnya

Setelah menarik garis dari metrik Grafana, status konsumer Kafka, hingga stack trace aplikasi Go, kita bisa membedah mengapa insiden ini bisa terjadi dengan sangat terstruktur. Masalah utamanya berakar pada tiga pilar yang salah konfigurasi: **Partisioning dan Consumer Group**, absennya **Idempotent Consumer Pattern**, serta buruknya penanganan **Poison Pill Messages**.

Pertama, mari bicara soal partisi. Dalam arsitektur Kafka, sebuah partisi hanya dapat dibaca oleh **satu konsumer dalam satu consumer group pada satu waktu**. Ketika kami mendesain topik `user-orders` dengan 32 partisi tetapi hanya mendaftarkan sedikit instance konsumer dengan strategi pembagian partisi (*partition assignment strategy*) default berbasis `RangeAssignor` atau `RoundRobinAssignor` tanpa pembatasan kustom, terjadilah fenomena *consumer starvation*. Satu instance konsumer dipaksa menelan beban dari seluruh partisi sekaligus.

Kedua, ketika konsumer mengalami timeout dan memicu rebalance, Kafka mengirim ulang pesan-pesan dari offset terakhir yang belum di-commit. Di sinilah bencana kedua terjadi: aplikasi Go kami tidak mengimplementasikan *Idempotent Consumer Pattern*. Pesan yang sama diproses ulang dari awal oleh database MySQL, menyebabkan duplikasi data transaksi dan memicu *Race Condition* serta kunci tabel (*table locks*), yang berujung pada error `Lock wait timeout exceeded`.

Ketiga, di tengah jutaan pesan yang mengalir, terdapat beberapa payload JSON yang korup atau tidak sesuai skema (malformed payload)—inilah yang kita sebut sebagai *Poison Pill*. Setiap kali konsumer Go mencoba membaca pesan beracun ini, fungsi deserialisasi gagal, aplikasi panik, pod restart, membaca pesan yang sama lagi, gagal lagi, dan terus menerus masuk ke dalam *infinite retry loop* yang melumpuhkan seluruh pipeline.

> [!TIP]
> Anggap partisi Kafka seperti jalur tol dan konsumer Go sebagai gardu tol. Jika Anda membuka 32 jalur tol (partisi) tetapi hanya mengoperasikan 1 petugas (konsumer) yang tidak punya mekanisme verifikasi tiket ganda (idempotensi) dan langsung pingsan saat melihat mobil rusak (poison pill), kemacetan total adalah jaminan mutlak.

Memahami akar masalah ini saja tidak cukup untuk menyelamatkan produksi malam itu. Kita membutuhkan tindakan darurat di level infrastruktur untuk menenangkan sistem secepat kilat.

## 4. Tindakan Darurat: Menghidupkan Kembali Layanan dalam Menit Ini

Dalam situasi darurat pukul 03:00 pagi, prioritas tertinggi bukanlah menulis kode yang elegan, melainkan menghentikan pendarahan, memulihkan ketersediaan layanan, dan menyelamatkan data yang masih bisa diselamatkan. Berikut adalah rangkaian perintah terminal darurat (Emergency Runbook) yang saya eksekusi untuk menstabilkan sistem dalam waktu kurang dari 60 detik.

Langkah pertama adalah melakukan *hard restart* pada deployment Go untuk membersihkan goroutine yang menggantung dan menghentikan loop error, sambil menurunkan jumlah replika sementara untuk mengurangi konkurensi ke database MySQL:

```bash
# Skrip One-Liner Darurat: Skala turunkan konsumer, bersihkan pod bermasalah, dan pantau ulang
kubectl scale deployment order-consumer-service --replicas=2 -n production
```

Namun, menurunkan replika saja tidak cukup untuk mengatasi beban antrean yang telanjur menumpuk. Kita perlu melakukan intervensi langsung pada konfigurasi Kafka konsumer di sisi kode Go. Berikut adalah cuplikan kode perbaikan cepat (*hotfix*) yang kami terapkan untuk mengatur ulang *session timeout*, *heartbeat interval*, dan menerapkan mekanisme *manual offset commit* yang aman menggunakan library `segmentio/kafka-go` atau `Shopify/Sarama`:

```go
package main

import (
	"context"
	"log"
	"time"

	"github.com/Shopify/sarama"
)

// Konfigurasi Konsumer Kafka yang Diperkuat untuk Mencegah Rebalance Storm
func SetupResilientConsumer() sarama.ConsumerGroup {
	config := sarama.NewConfig()
	config.Version = sarama.V2_8_0_0
	
	// Pengaturan waktu yang lebih toleran terhadap latensi database
	config.Consumer.Group.Session.Timeout = 45 * time.Second
	config.Consumer.Group.Heartbeat.Interval = 15 * time.Second
	config.Consumer.MaxProcessingTime = 2 * time.Minute
	
	// Mematikan auto-commit untuk mencegah kehilangan data saat terjadi panic
	config.Consumer.Offsets.Auto.Commit.Enable = false
	config.Consumer.Offsets.Initial = sarama.OffsetOldest
	
	// Menggunakan sticky assignor untuk distribusi partisi yang lebih stabil
	config.Consumer.Group.Partition.Assignors = []sarama.BalanceStrategy{
		sarama.NewBalanceStrategySticky(),
	}

	return nil // Kembalikan objek consumer group yang terinisialisasi
}
```

Selain itu, untuk mengatasi *Poison Pill*, kami langsung menyambungkan skrip pemindahan pesan korup ke topik khusus (*Dead Letter Queue / DLQ*) menggunakan utilitas CLI Kafka agar worker tidak terjebak pada satu pesan rusak:

```bash
# Melewati offset yang bermasalah secara manual jika diperlukan (Gunakan dengan sangat hati-hati!)
/opt/kafka/bin/kafka-consumer-groups.sh \
  --bootstrap-server kafka-broker-01.internal:9092 \
  --group order-fulfillment-group \
  --topic user-orders \
  --reset-offsets --to-offset 1450200 --execute
```

Setelah perintah-perintah darurat ini dieksekusi, metrik Grafana perlahan-lahan kembali melandai. Error 504 menghilang, latensi API turun drastis ke angka normal, dan antrean Kafka mulai terkikis habis. Namun, pekerjaan rumah sejati baru saja dimulai: bagaimana memastikan insiden memalukan ini tidak pernah terulang kembali di masa depan melalui kebijakan arsitektural yang defensif.

## 5. Kebijakan Retrospektif: Menambal Celah agar Tak Pernah Terulang

Pelajaran termahal adalah pelajaran yang dibayar dengan jam tidur tengah malam. Selepas insiden tersebut, tim engineering menggelar sesi Post-Mortem formal untuk merombak total fondasi arsitektur event-driven kami. Kami merumuskan tiga kebijakan defensif utama yang wajib dipatuhi oleh seluruh engineer di masa mendatang.

Pertama, penerapan **Idempotent Consumer Pattern** secara mutlak di level aplikasi Go. Setiap event yang masuk kini wajib menyertakan `Idempotency-Key` (UUIDv4) yang disimpan ke dalam Redis dengan masa kedaluwarsa (TTL) 24 jam sebelum proses database dijalankan. Jika pesan dengan kunci yang sama tiba kembali akibat retry atau rebalance, konsumer akan langsung mendeteksi duplikasi tersebut, mengabaikan eksekusi database, dan langsung melakukan commit offset tanpa error.

```go
// Implementasi Idempotency Check menggunakan Redis di dalam Konsumer Go
func ProcessOrderEvent(ctx context.Context, redisClient *redis.Client, event OrderEvent) error {
	cacheKey := fmt.Sprintf("processed:order:%s", event.ID)
	
	// Cek apakah event sudah pernah diproses menggunakan SETNX (Set if Not Exists)
	set, err := redisClient.SetNX(ctx, cacheKey, "1", 24*time.Hour).Result()
	if err != nil {
		return fmt.Errorf("gagal mengakses redis untuk idempotency: %w", err)
	}
	
	if !set {
		// Event sudah pernah diproses sebelumnya, abaikan dengan aman (Idempotent success)
		log.Printf("INFO: Duplikat event terdeteksi dan diabaikan: %s", event.ID)
		return nil
	}
	
	// Lanjutkan proses bisnis utama (Database Write)
	return executeDatabaseTransaction(event)
}
```

Kedua, kami membangun mekanisme **Dead Letter Queue (DLQ)** yang otomatis. Setiap kali konsumer Go gagal memproses sebuah pesan setelah 3 kali percobaan (*max retries*), pesan tersebut tidak dibiarkan menyebabkan *panic* atau *infinite loop*. Alih-alih demikian, pesan beserta metadata error-nya akan otomatis dilempar ke topik Kafka sekunder bernama `user-orders-dlq` dan mengirimkan notifikasi instan ke channel Slack tim engineering.

Ketiga, kami merapikan korelasi antara jumlah partisi Kafka dan pod konsumer Kubernetes. Kami menetapkan aturan baku: jumlah pod konsumer Go harus selalu lebih kecil atau sama dengan jumlah partisi Kafka, dan dikelola menggunakan Horizontal Pod Autoscaler (HPA) berbasis kustom metrik `kafka_consumergroup_lag` dari Prometheus, bukan sekadar menggunakan CPU utilization yang kerap menipu.

Namun, infrastruktur yang tangguh tidak hanya dibangun dari kode yang kokoh, melainkan juga dari sistem pengawasan dini yang siaga 24/7. Di sinilah pentingnya memasang alarm yang cerdas.

## 6. Pasang Alarm: Otomasi Deteksi Dini untuk Tim On-Call

Agar tidak ada lagi kepanikan pukul 02:00 pagi akibat lonjakan lag yang diam-diam merayap, kami melengkapi sistem observabilitas kami dengan aturan alert Prometheus (*Prometheus Alerting Rules*) yang terintegrasi langsung dengan PagerDuty dan webhook Slack. Sistem alarm ini dirancang untuk mendeteksi anomali sejak tanda-tanda awal muncul, jauh sebelum latensi berdampak pada pengguna akhir.

Berikut adalah file konfigurasi valid `prometheus-rules.yml` yang kami gunakan untuk mendeteksi *consumer lag* yang tidak wajar dan kegagalan beruntun pada konsumer Go:

```yaml
groups:
  - name: kafka-consumer-alerts
    rules:
      - alert: KafkaConsumerLagHigh
        expr: sum(kafka_consumergroup_lag{group="order-fulfillment-group"}) by (topic) > 10000
        for: 2m
        labels:
          severity: critical
          team: backend-infrastructure
        annotations:
          summary: "Consumer lag sangat tinggi pada topik {{ $labels.topic }}"
          description: "Consumer group order-fulfillment-group mengalami lag lebih dari 10.000 pesan selama 2 menit berturut-turut. Segera periksa kesehatan konsumer Go!"

      - alert: KafkaConsumerRestartLoop
        expr: rate(kube_pod_container_status_restarts_total{container="order-consumer"}[5m]) > 0.05
        for: 1m
        labels:
          severity: warning
          team: backend-infrastructure
        annotations:
          summary: "Pod konsumer Go mengalami restart berulang (CrashLoopBackOff)"
          description: "Container order-consumer sering melakukan restart, indikasi kuat adanya Poison Pill atau kegagalan koneksi database."
```

Dengan konfigurasi aturan alarm di atas, tim on-call kini memiliki waktu bernapas yang cukup untuk bertindak secara proaktif sebelum sebuah anomali berubah menjadi pemadaman total layanan produksi. Arsitektur event-driven berbasis Kafka dan Go kini berdiri kokoh, siap menghadapi terjangan traffic terbesar sekalipun dengan kepala dingin.