# Mendesain Sistem Event-Driven Skala Besar dengan Kafka dan Go

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

---

# Mendesain Sistem Event-Driven Skala Besar dengan Kafka dan Go

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

Ingatkah Anda saat sistem web monolitik kita mulai megap-megap ketika trafik melonjak lima kali lipat dalam hitungan detik? Dulu, ketika sistem masih mengandalkan komunikasi sinkron HTTP/REST antar layanan—misalnya backend Go memanggil API PHP, yang kemudian menulis langsung ke database MySQL—sebuah lonjakan kecil saja bisa memicu efek domino yang mematikan. Koneksi database habis karena *connection pool* penuh, PHP-FPM macet menunggu respon, Nginx mulai melempar *504 Gateway Timeouts*, dan Cloudflare mencatat lonjakan error dari pengguna.

Masalah utamanya bukan pada bahasa pemrogramannya, melainkan pada kopling temporal (*temporal coupling*). Layanan pengirim harus menunggu layanan penerima hidup dan merespon detik itu juga. Ketika salah satu komponen di tengah rantai melambat, seluruh ekosistem ikut tersedat. Di sinilah kita sadar bahwa arsitektur berbasis event dengan Apache Kafka bukan sekadar tren, melainkan pertahanan hidup mutlak untuk memisahkan beban kerja (*decouple workloads*) dan memastikan sistem tetap bernapas saat badai trafik menerjang.

Tetapi memindahkan data ke Kafka tidak serta-merta menyelesaikan semua masalah. Jika salah merancang partisi atau salah mengonfigurasi *consumer group*, kita justru memindahkan kerumitan dari database ke dalam klaster pesan kita sendiri. Mari kita bedah bagaimana komponen-komponen ini berinteraksi tanpa sihir, melainkan murni rekayasa sistem yang terukur.

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

Mari kita bayangkan Kafka bukan sebagai kotak hitam misterius, tetapi sebagai buku besar akuntansi digital yang sangat cepat dan persisten. Setiap kali sebuah event terjadi—misalnya seorang pengguna melakukan *checkout* keranjang belanja di frontend web—backend Go kita mempublikasikan event tersebut ke sebuah *topic* tertentu.

Di dalam Kafka, data ini tidak langsung dihapus setelah dibaca. Data ditulis secara berurutan (*sequential write*) ke disk, memanfaatkan kemampuan OS page cache secara maksimal sehingga performanya mendekati memori RAM. Setiap *topic* dipecah menjadi beberapa *partition*. Partisi inilah unit skalabilitas horizontal kita. Jika kita memiliki empat partisi, kita bisa menjalankan hingga empat instance *consumer* dalam satu *consumer group* yang bekerja secara paralel tanpa saling berebut pesan yang sama.

```
[Frontend (JS/Nginx)] --> (HTTP POST) --> [Backend Go API]
                                              |
                                              v (Producer)
                                     [Apache Kafka Topic]
                                       /      |      \
                         (Partition 0)/  (Part 1)|  (Part 2)
                                     v        v        v
                                  [Go Worker Node A]
                                  [Go Worker Node B]
                                  [Go Worker Node C]
```

Ketika *consumer* membaca pesan, mereka melacak posisi baca melalui *offset*. Masalah klasik di lapangan muncul saat sebuah layanan *consumer* mendadak *crash* di tengah jalan. Kafka harus melakukan proses *rebalancing*—menata ulang siapa yang membaca partisi mana. Jika tidak ditangani dengan kepala dingin, proses ini bisa memicu *rebalance storm* yang membuat *consumer* terus-menerus sibuk berunding alih-alih memproses data bisnis.

## 3. Pilihan Pendekatan & Kompromi yang Harus Diambil

Sebelum kita menulis baris kode pertama, kita harus jujur pada diri sendiri mengenai kompromi arsitektural yang dipilih. Tidak ada peluru perak dalam rekayasa sistem terdistribusi. Kita dihadapkan pada pilihan antara *at-least-once delivery* atau *at-most-once delivery*, serta bagaimana mencapai *exactly-once processing* semu.

Dalam ekosistem Go, pustaka populer untuk berinteraksi dengan Kafka umumnya terbagi dua: `segmentio/kafka-go` dan `confluentinc/confluent-kafka-go`. 

| Parameter Perbandingan | `segmentio/kafka-go` | `confluentinc/confluent-kafka-go` |
| :--- | :--- | :--- |
| **Dependensi C/C++** | Murni Go (*Pure Go*), tanpa pustaka C | Membutuhkan *librdkafka* (C-based) |
| **Kemudahan Cross-Compile** | Sangat mudah (standalone binary) | Memerlukan toolchain C dan konfigurasi build khusus |
| **Performa & Fitur Lanjut** | Sangat baik untuk kasus umum | Superior untuk konfigurasi enterprise dan stabilitas C-level |
| **Manajemen Memori** | Mengandalkan Garbage Collector Go | Memerlukan perhatian ekstra pada alokasi memori C |

Untuk sebagian besar layanan mikro berbasis Go di perusahaan kita, `segmentio/kafka-go` dipilih karena kemudahan *deployment* ke kontainer Docker tanpa merusak ukuran image dengan dependensi pustaka C. Namun, untuk volume event di atas 100.000 pesan per detik per node, `confluentinc/confluent-kafka-go` sering kali menjadi penyelamat stabilitas.

## 4. Meracik Lingkungan: Persiapan & Prasyarat Lapangan

Mari kita siapkan lingkungan lokal yang mencerminkan infrastruktur produksi. Kita akan menggunakan Docker Compose untuk menjalankan ZooKeeper, Kafka, dan MinIO (sebagai penyimpanan arsip objek jika diperlukan). 

Simpan konfigurasi berikut sebagai `docker-compose.yml` di direktori kerja Anda:

```yaml
version: '3.8'

services:
  zookeeper:
  image: confluentinc/cp-zookeeper:7.3.0
  environment:
    ZOOKEEPER_CLIENT_PORT: 2181
    ZOOKEEPER_TICK_TIME: 2000

  kafka:
  image: confluentinc/cp-kafka:7.3.0
  depends_on:
    - zookeeper
  ports:
    - "9092:9092"
  environment:
    KAFKA_BROKER_ID: 1
    KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181'
    KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
    KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
    KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
    KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
```

Jalankan klaster lokal ini dengan perintah terminal standar:

```bash
docker-compose up -d
```

Pastikan port `9092` terbuka dan responsif sebelum kita mulai menulis kode produsen dan konsumen dalam bahasa Go.

## 5. Langkah Implementasi: Membangun Solusi yang Tangguh

Sekarang, mari kita bangun kerangka aplikasi Go yang mengimplementasikan pola kunci: penanganan *consumer group rebalancing*, manajemen *poisoned message* melalui *Dead Letter Queue* (DLQ), dan pola *idempotency* agar event yang diproses dua kali tidak merusak saldo atau data pengguna.

Inisialisasi modul Go baru di direktori Anda:

```bash
go mod init kafka-go-engine
go get github.com/segmentio/kafka-go
go get github.com/google/uuid
```

Kita akan membuat struktur direktori yang rapi: `cmd/producer/main.go` dan `cmd/consumer/main.go`. Mari kita pastikan implementasi ini tahan banting terhadap kegagalan jaringan dan gangguan *rebalancing*.

## 6. Membedah Baris Kode Krusial yang Kerap Menjebak

Berikut adalah implementasi *consumer* Go yang menerapkan *idempotency check* menggunakan Redis dan penanganan DLQ untuk pesan beracun (*poisoned messages*) yang gagal diproses berkali-kali.

Buat berkas `cmd/consumer/main.go`:

```go
package main

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

	"github.com/segmentio/kafka-go"
)

type EventPayload struct {
	EventID   string `json:"event_id"`
	UserID    string `json:"user_id"`
	Action    string `json:"action"`
	Timestamp int64  `json:"timestamp"`
}

// Simulasi pengecekan idempotensi menggunakan penyimpanan key-value (misal: Redis)
func isProcessed(eventID string) bool {
	// Di dunia nyata, periksa ke Redis: EXISTS processed_event:{eventID}
	return false
}

func markAsProcessed(eventID string) {
	// Di dunia nyata, simpan ke Redis dengan TTL 24 jam: SETEX processed_event:{eventID} 86400 1
}

func sendToDLQ(ctx context.Context, writer *kafka.Writer, msg kafka.Message, errReason string) {
	// Kirim pesan gagal ke topic Dead Letter Queue untuk dianalisis nanti
	DLQMessage := kafka.Message{
		Key:   msg.Key,
		Value: msg.Value,
		Headers: []kafka.Header{
			{Key: "error_reason", Value: []byte(errReason)},
			{Key: "failed_at", Value: []byte(time.Now().Format(time.RFC3339))},
		},
	}

	if err := writer.WriteMessages(ctx, DLQMessage); err != nil {
		log.Printf("[CRITICAL] Gagal menulis ke DLQ: %v", err)
	}
}

func main() {
	ctx := context.Background()
	brokerAddress := "localhost:9092"
	topic := "user-actions"
	dlqTopic := "user-actions-dlq"

	// Inisialisasi Consumer Group
	r := kafka.NewReader(kafka.ReaderConfig{
		Brokers:        []string{brokerAddress},
		Topic:          topic,
		GroupID:        "user-worker-group",
		MinBytes:       10e3, // 10KB
		MaxBytes:       10e6, // 10MB
		CommitInterval: time.Second,
		StartOffset:    kafka.LastOffset,
	})

	// Inisialisasi Producer khusus untuk DLQ
	dlqWriter := &kafka.Writer{
		Addr:     kafka.TCP(brokerAddress),
		Topic:    dlqTopic,
		Balancer: &kafka.LeastBytes{},
	}
	defer dlqWriter.Close()

	log.Println("Consumer berjalan, mendengarkan event...")

	for {
		m, err := r.FetchMessage(ctx)
		if err != nil {
			log.Printf("[ERROR] Gagal mengambil pesan: %v", err)
			break
		}

		var payload EventPayload
		if err := json.Unmarshal(m.Value, &payload); err != nil {
			log.Printf("[WARNING] Pesan rusak (poisoned message): %v. Mengirim ke DLQ.", err)
			sendToDLQ(ctx, dlqWriter, m, fmt.Sprintf("Unmarshal error: %v", err))
			_ = r.CommitMessages(ctx, m)
			continue
		}

		// Pola Idempotensi
		if isProcessed(payload.EventID) {
			log.Printf("[INFO] Event duplikat terdeteksi dan diabaikan: %s", payload.EventID)
			_ = r.CommitMessages(ctx, m)
			continue
		}

		// Proses bisnis utama
		err = processBusinessLogic(payload)
		if err != nil {
			log.Printf("[ERROR] Gagal memproses event %s: %v", payload.EventID, err)
			// Jika kegagalan bersifat sementara (misal: DB timeout), kita bisa retry atau lempar ke DLQ
			sendToDLQ(ctx, dlqWriter, m, err.Error())
		}

		markAsProcessed(payload.EventID)

		// Commit offset secara manual setelah sukses diproses
		if err := r.CommitMessages(ctx, m); err != nil {
			log.Printf("[ERROR] Gagal melakukan commit offset: %v", err)
		}
	}
}

func processBusinessLogic(p EventPayload) error {
	// Simulasi logika bisnis
	time.Sleep(50 * time.Millisecond)
	return nil
}
```

> [!IMPORTANT]
> Jangan pernah melakukan *commit offset* secara otomatis (`CommitInterval` berbasis waktu tanpa verifikasi sukses) jika logika bisnis Anda belum sepenuhnya berhasil mengeksekusi penyimpanan data. Hal ini mencegah kehilangan data jika aplikasi mendadak mengalami *kernel panic* atau OOM killer.

## 7. Bertahan di Bawah Tekanan: Skenario Traffic Nyata

Ketika *flash sale* dimulai, trafik backend melonjak dari ratusan request menjadi ratusan ribu event per detik. Pada titik ini, *consumer group rebalancing* menjadi momok menakutkan. Jika sebuah node pekerja terputus sesaat karena jeda jaringan, Kafka memicu rebalance yang menghentikan seluruh proses konsumsi (*stop-the-world* sesaat) hingga pembagian partisi selesai.

Untuk meredam dampak ini di lingkungan produksi, lakukan pengaturan parameter berikut:

1. Tingkatkan `session.timeout.ms` dan `max.poll.interval.ms` pada konfigurasi Kafka agar pekerja memiliki waktu lebih longgar untuk menyelesaikan tugas berat tanpa dianggap mati oleh koordinator Kafka.
2. Terapkan mekanisme *graceful shutdown* pada aplikasi Go menggunakan `signal.Notify` agar ketika kontainer menerima sinyal `SIGTERM` dari Kubernetes, ia sempat menyelesaikan proses batch yang sedang berjalan dan melakukan *commit offset* terakhir sebelum mati dengan tenang.

## 8. Jebakan Klasik & Cara Menghindarinya Sebelum Meledak

Pengalaman bertahun-tahun mengawal infrastruktur skala besar mengajarkan kita beberapa kesalahan fatal yang sering dilakukan engineer pemula:

* **Mengabaikan ukuran pesan (Payload Size):** Kafka dirancang untuk event berukuran kecil (di bawah 1MB). Jangan pernah mengirim data biner besar atau *blob* dokumen PDF lewat Kafka. Simpan file besar ke S3/MinIO, lalu kirim *URL pointer*-nya saja melalui event Kafka.
* **Kunci Partisi yang Buruk (*Partition Key Skew*):** Jika Anda menggunakan ID pengguna yang tidak merata (misalnya 90% trafik berasal dari satu akun korporat raksasa), seluruh beban partisi akan menumpuk pada satu broker saja. Gunakan kombinasi *UUID* atau *hashing* yang sehat untuk kunci partisi.
* **Tidak Memantau Consumer Lag:** Kekurangan memori atau lambatnya query database akan menyebabkan *consumer lag* menumpuk. Selalu pasang *exporter Prometheus* dan buat dasbor Grafana untuk memantau metrik `kafka_consumergroup_lag` secara real-time dengan *alerting* ke PagerDuty.

## 9. Uji Nyali: Mengukur Performa di Titik Batas

Sebelum merilis sistem ke publik, lakukan pengujian beban (*load testing*) menggunakan perangkat seperti `k6` atau utilitas bawaan Kafka `kafka-producer-perf-test.sh` untuk mengetahui titik jenuh klaster Anda.

Jalankan simulasi pengujian produsen secara lokal:

```bash
docker exec -it kafka kafka-producer-perf-test \
  --topic user-actions \
  --num-records 100000 \
  --record-size 500 \
  --throughput 5000 \
  --producer-props bootstrap.servers=localhost:9092
```

Amati performa CPU pada wadah Docker broker dan pastikan latensi p99 tetap berada di bawah 50 milidetik. Jika latensi meroket, saatnya menambah jumlah partisi atau meningkatkan spesifikasi EBS volume pada server Kafka Anda.

## 10. Catatan Lapangan & Checklist Sebelum Rilis

Sebagai penutup, sebelum Anda menekan tombol *merge* dan menaikkan layanan ini ke klaster Kubernetes produksi, pastikan *checklist* operasional berikut telah centang semua:

- [ ] Topik Kafka telah dibuat dengan jumlah partisi yang memperhitungkan pertumbuhan masa depan (minimal 6-12 partisi untuk trafik menengah).
- [ ] Mekanisme *Dead Letter Queue* (DLQ) aktif dan terhubung ke sistem *alerting* tim rekayasa.
- [ ] Pola *Idempotency* teruji dengan penyimpanan Redis ber-TTL untuk mencegah transaksi ganda.
- [ ] Konfigurasi *Graceful Shutdown* di Go teruji dengan simulasi penghentian pod mendadak.
- [ ] Dasbor Grafana untuk *Consumer Lag* dan *Broker Disk I/O* sudah aktif dan dipantau 24/7.

Dengan fondasi arsitektur yang kokoh ini, sistem event-driven Anda tidak hanya akan bertahan dari lonjakan trafik brutal, tetapi juga tumbuh menjadi salah satu porsi infrastruktur paling diandalkan di perusahaan.
