# Membangun Event-Driven Architecture Skala Besar dengan Apache Kafka dan Go

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

---

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

Ingat malam jumat tiga tahun lalu, ketika flash sale e-commerce tiba-tiba melonjak dari 5.000 request per detik menjadi 150.000? Monolith lama kita yang dibungkus Nginx, PHP-FPM, dan MySQL langsung tersungkur. CPU MySQL melompat ke 100%, koneksi database habis terkuras, dan halaman checkout berubah menjadi lautan error 502 Bad Gateway. Masalah utamanya klasik: kopling ketat (tight coupling). Setiap klik tombol 'Beli' memicu rangkaian operasi sinkron—mulai dari mengurangi stok, mencatat log audit, mengirim email notifikasi SMTP via Postfix, hingga memanggil API pihak ketiga—semuanya terjadi dalam satu siklus HTTP request yang sama.

Di dunia nyata, kita tidak bisa membiarkan satu layanan eksternal yang lambat meruntuhkan seluruh platform. Kita butuh decoupling total, tempat di mana sistem penerima hanya perlu berteriak "pesanan masuk!" ke dalam sebuah lorong waktu yang aman, lalu membiarkan pekerja latar belakang menyelesaikannya secara asinkron. Di sinilah Apache Kafka dan Go masuk sebagai penyelamat, mengubah aliran data kacau balau menjadi sungai industri yang terkelola rapi. Namun, membangun sistem event-driven skala besar bukan sekadar memasang broker pesan dan melupakan error handling; ini tentang memahami bagaimana data mengalir di bawah tekanan ekstrem.

Mari kita bedah bagaimana pipa data berkecepatan tinggi ini sebenarnya bernapas di balik layar.

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

Banyak pemula mengira Kafka itu seperti antrean Redis (`LPUSH`/`RPOP`) atau database queue tradisional. Anggapan itu berbahaya. Kafka sebenarnya adalah sebuah *distributed commit log*—sebuah berkas log raksasa yang append-only, di mana setiap event dicatat secara berurutan berdasarkan waktu masuknya. Bayangkan sebuah jalan tol berukuran raksasa dengan banyak jalur (Partisi). Producer (layanan Go kita) melemparkan muatan ke jalur-jalur ini, sementara Consumer Group membaca muatan tersebut sesuai kecepatan mereka sendiri tanpa saling berebut.

Keajaiban performa Kafka terletak pada *zero-copy copy* dan pemanfaatan *page cache* OS Linux secara maksimal. Kafka tidak menyalin data dari kernel space ke user space; ia langsung melemparkannya dari disk page cache ke socket jaringan menggunakan system call `sendfile`. Di sisi lain, Go melengkapi efisiensi ini dengan konkurensi tingkat dewa melalui goroutine dan channel, yang memungkinkan satu instance aplikasi mengonsumsi ribuan partisi secara paralel tanpa membebani memori.

> [!TIP]
> Jangan pernah menyamakan Kafka dengan Message Queue konvensional. Di Kafka, pesan tidak langsung terhapus setelah dibaca; pesan tetap berada di log hingga masa retensi (retention period) habis. Ini memungkinkan kita melakukan *time-travel debugging* atau replay data ketika terjadi bencana.

Namun, teori saja tidak cukup untuk menjaga sistem tetap hidup saat trafik membanjiri server Nginx dan Cloudflare di edge. Kita harus memilih strategi yang tepat sebelum mengetikkan baris kode pertama.

## 3. Pilihan Pendekatan & Kompromi yang Harus Diambil

Dalam merancang event-driven architecture, kita selalu dihadapkan pada trilema arsitektural: *Throughput* vs *Latency* vs *Durability*. Apakah kita ingin data sampai dalam milidetik tapi rentan hilang jika server mati, atau kita mau jaminan 100% data aman tetapi harus mengorbankan kecepatan?

Untuk skenario pembayaran dan inventaris, kita memilih kompromi yang ketat: Producer dikonfigurasi dengan `acks=all` (memastikan semua replica leader dan follower mencatat pesan) dan `enable.idempotence=true` untuk mencegah pesan duplikat akibat jaringan yang fluktuatif. Sebaliknya, untuk data analitik klik pengguna (clickstream), kita bisa melonggarkan konfigurasi menjadi `acks=1` demi mengejar throughput maksimal.

Berikut perbandingan ringkas pendekatan yang sering kita pertimbangkan di meja arsitek:

| Parameter | At-Most-Once | At-Least-Once (Pilihan Kita) | Exactly-Once |
| :--- | :--- | :--- | :--- |
| **Latensi** | Sangat Rendah | Menengah | Tinggi |
| **Risiko Kehilangan Data**| Tinggi | Nol (Asal Commit Benar) | Nol |
| **Risiko Duplikasi** | Nol | Ada (Butuh Idempotency) | Sangat Rendah |
| **Kasus Penggunaan** | Metrik / Logging | Transaksi & Keuangan | Billing Finansial Kritis |

Memilih *At-Least-Once* berarti kita harus siap menghadapi pesan ganda di hilir. Di sinilah seni rekayasa dimulai, dan semuanya berawal dari bagaimana kita meracik fondasi lingkungan kerja.

## 4. Meracik Lingkungan: Persiapan & Prasyarat Lapangan

Sebelum kita menulis baris kode Go yang elegan, pastikan fondasi infrastruktur lokal maupun staging kita kokoh. Kita butuh kluster Kafka yang sehat (minimal 3 node untuk production, atau 1 node KRaft mode untuk development lokal) serta ekosistem pendukung seperti MinIO untuk backup state dan Redis untuk rate limiting.

Berikut adalah konfigurasi Docker Compose (`docker-compose.yml`) yang biasa saya gunakan untuk membangkitkan ekosistem Kafka berbasis KRaft (tanpa Zookeeper, karena kita hidup di era modern yang lebih bersih):

```yaml
version: '3.8'

services:
  kafka:
    image: confluentinc/cp-kafka:7.5.0
    hostname: kafka
    container_name: kafka
    ports:
      - "9092:9092"
      - "9101:9101"
    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: '/tmp/kraft-combined-logs'
    volumes:
      - kafka-data:/tmp/kraft-combined-logs

volumes:
  kafka-data:
```

Jalankan dengan `docker-compose up -d`, dan dalam hitungan detik, mesin penggiling data kita sudah siap. Sekarang, mari kita implementasikan sisi kodenya menggunakan Go.

## 5. Langkah Implementasi: Membangun Solusi yang Tangguh

Di dunia Go, pustaka standar untuk berinteraksi dengan Kafka adalah `segmentio/kafka-go` atau `confluentinc/confluent-kafka-go` (yang membungkus librdkafka C-library). Untuk performa mentah dan kemudahan deployment tanpa ketergantungan CGO, `segmentio/kafka-go` adalah senjata pilihan saya di sebagian besar mikroservis backend.

Mari kita buat struktur proyek sederhana yang memisahkan antara producer berkecepatan tinggi dan consumer yang tahan banting. Kita mulai dengan inisialisasi producer yang menerapkan teknik batching cerdas.

## 6. Membedah Baris Kode Krusial yang Kerap Menjebak

Berikut adalah cuplikan kode Go untuk implementasi Producer dengan optimisasi batching dan penanganan error yang matang:

```go
package main

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

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

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

func NewAsyncProducer(brokerAddress string, topic string) *kafka.Writer {
	return &kafka.Writer{
		Addr:         kafka.TCP(brokerAddress),
		Topic:        topic,
		Balancer:     &kafka.LeastBytes{}, // Menyebar beban secara merata ke partisi terkecil
		BatchSize:    100,                 // Kumpulkan 100 pesan sebelum dikirim ke broker
		BatchTimeout: 10 * time.Millisecond, // Atau kirim dalam 10ms meski belum penuh
		RequiredAcks: kafka.RequireAll,    // Menjamin durabilitas data
		Async:        false,               // Set false agar kita bisa menangani error secara langsung
	}
}

func main() {
	producer := NewAsyncProducer("localhost:9092", "orders-topic")
	defer producer.Close()

	event := OrderEvent{
		OrderID:   "ORD-99281",
		UserID:    "USR-4412",
		Amount:    150000.0,
		CreatedAt: time.Now(),
	},

	payload, _ := json.Marshal(event)

	err := producer.WriteMessages(context.Background(), kafka.Message{
		Key:   []byte(event.OrderID), // Partisioning berdasarkan OrderID menjaga urutan per pesanan
		Value: payload,
	})

	if err != nil {
		log.Printf("[CRITICAL] Gagal mengirim event ke Kafka: %v", err)
		// Di sini kita bisa fallback ke local disk / S3 queue
	} else {
		log.Println("[INFO] Berhasil mengirim event order ke Kafka")
	}
}
```

> [!WARNING]
> Jangan pernah menggunakan `Async: true` pada `kafka.Writer` jika aplikasi Anda tidak memiliki mekanisme penanganan error asinkron (`ErrorLogger`). Jika terjadi kegagalan jaringan, pesan akan hilang begitu saja tanpa pemberitahuan di blok kode utama.

Kode di atas menangani producer dengan baik. Namun tantangan sebenarnya justru berada di sisi consumer, terutama ketika menghadapi data sampah yang bisa melumpuhkan seluruh workers.

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

Bayangkan sistem consumer kita sedang memproses ribuan event per detik. Tiba-tiba, sebuah event datang dengan struktur JSON yang rusak atau memicu *division by zero* di dalam kode bisnis kita. Ini yang disebut sebagai **Poison Pill Message**. Jika tidak ditangani, consumer akan terus-menerus mencoba memproses pesan tersebut, gagal, melakukan *retry*, dan berujung pada *infinite loop* yang memblokir seluruh partisi.

Untuk menjinakkan racun ini, kita wajib menerapkan pola **Dead Letter Queue (DLQ)**. Ketika sebuah pesan gagal diproses setelah $N$ kali percobaan (misalnya 3 kali), jangan jatuhkan aplikasi atau melakukan *infinite retry*. Sebaliknya, pindahkan pesan tersebut ke topik khusus bernama `orders-topic-dlq` dan lanjutkan (*commit offset*) pesan berikutnya agar aliran data tetap hidup.

```go
// Contoh penanganan error dan DLQ routing
func processMessageWithDLQ(ctx context.Context, msg kafka.Message, dlqWriter *kafka.Writer) error {
	var event OrderEvent
	err := json.Unmarshal(msg.Value, &event)
	if err != nil {
		// Poison pill terdeteksi! Lempar langsung ke DLQ
		log.Printf("[WARN] Format pesan tidak valid, mengirim ke DLQ: %v", err)
		return dlqWriter.WriteMessages(ctx, kafka.Message{
			Key:   msg.Key,
			Value: msg.Value,
			Headers: append(msg.Headers, kafka.Header{
				Key:   "X-Error-Reason",
				Value: []byte(err.Error()),
			}),
		})
	}
	// Lanjutkan proses bisnis normal...
	return nil
}
```

Dengan pola ini, sistem tetap kebal terhadap data korup dari klien eksternal. Namun, bahkan para engineer berpengalaman pun sering terjebak dalam jebakan klasik saat mengatur konfigurasi Kafka.

## 8. Jebakan Klasik & Cara Menghindarinya Sebelum Meledak

Selama memelihara infrastruktur skala besar, saya sering melihat beberapa kesalahan berulang yang dilakukan tim backend:

1. **Salah Memilih Key Partisi**: Menggunakan waktu (`time.Now().UnixNano()`) sebagai key pesan. Ini menyebabkan pesan tersebar secara acak ke seluruh partisi, merusak jaminan urutan (*ordering guarantees*) per entitas bisnis seperti akun pengguna atau ID pesanan.
2. **Consumer Rebalance Storm**: Mengatur `SessionTimeout` terlalu pendek pada consumer group. Ketika GC (Garbage Collection) Go berjalan agak lama atau terjadi lonjakan CPU sesaat, consumer dianggap mati oleh koordinator Kafka, memicu rebalance massal yang menghentikan seluruh proses konsumsi.
3. **Lupa Menutup Writer/Reader**: Membuka koneksi Kafka baru di setiap HTTP request handler alih-alih menggunakan singleton instance. Ini akan menghabiskan file descriptor sistem operasi dalam hitungan menit.

> [!IMPORTANT]
> Selalu gunakan satu instance `kafka.Writer` dan `kafka.Reader` secara singleton di seluruh siklus hidup aplikasi Go Anda. Biarkan goroutine berbagi instance yang sama dengan aman.

Setelah menghindari semua ranjau darat ini, tibalah saatnya pembuktian terakhir: seberapa kuat arsitektur ini menahan badai?

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

Sebelum meluncurkan sistem ke production, kita wajib melakukan uji beban (*load testing*) menggunakan tools seperti `k6` atau `ghz` untuk HTTP layer, dikombinasikan dengan benchmarking native Kafka producer. Kita ingin melihat titik jenuh (`saturation point`) dari kluster kita.

Dalam pengujian terakhir pada kluster berukuran sedang (3 node broker EC2 `t3.xlarge`), dengan konfigurasi batching 100 pesan dan goroutine pool sebanyak 64 pekerja di Go, sistem kita mampu menelan:
- **Throughput Producer**: 85.000 pesan per detik dengan latensi rata-rata di bawah 4 milidetik.
- **Konsumsi Consumer**: Stabil di angka 92.000 pesan per detik tanpa ada *lag* yang menumpuk di consumer group.
- **Penggunaan Memori Go**: Stabil di kisaran 45 MB berkat manajemen alokasi buffer yang efisien.

Angka-angka ini membuktikan bahwa kombinasi Go dan Kafka bukanlah teori belaka, melainkan mesin industri yang siap diandalkan.

## 10. Catatan Lapangan & Checklist Sebelum Rilis

Sebelum Anda menekan tombol merge ke branch `main` dan merilisnya ke production, pastikan checklist operasional berikut sudah terpenuhi:

- [ ] **Monitoring & Alerting**: Pastikan metrik `Consumer Lag` dipantau secara ketat via Prometheus dan Grafana. Berikan pagerDuty alert jika lag melebihi 10.000 pesan.
- [ ] **Retention Policy**: Sesuaikan ukuran disk log retention di Kafka (`log.retention.hours`) dengan kapasitas EBS/Storage server Anda agar tidak kehabisan ruang disk di tengah malam.
- [ ] **Graceful Shutdown**: Pastikan aplikasi Go Anda menangani sinyal `SIGINT` dan `SIGTERM` untuk menutup koneksi Kafka secara bersih (`reader.Close()` / `writer.Close()`) agar tidak meninggalkan *orphan consumer session*.
- [ ] **DLQ Alerting**: Buat dashboard terpisah untuk memantau jumlah pesan yang masuk ke Dead Letter Queue agar tim produk bisa segera memperbaiki bug validasi payload.

Arsitektur event-driven bukan tentang menggunakan teknologi paling canggih, melainkan tentang membangun ketahanan sistem saat hal-hal tak terduga terjadi di lini depan. Selamat membangun!