Apache Kafka dapat mempertahankan 2 juta tulis per detik pada tiga mesin komoditas — tolok ukur yang dipublikasikan LinkedIn yang masih berlaku hingga hari ini. Angka itu sering membuat developer langsung memilih Kafka. Namun saya telah belajar bahwa throughput bukan pertanyaan pertama yang harus diajukan. Pertanyaan pertama adalah: apakah masalah Anda benar-benar membutuhkan event bus? Banyak beban kerja ERP, seperti pemicu email dan pembuatan invoice, sudah terlayani dengan baik oleh antrian berbasis database sederhana — dan itu adalah keputusan yang tepat untuk beban kerja tersebut. Kafka memperoleh kompleksitasnya ketika beberapa layanan perlu bereaksi terhadap event yang sama secara independen, ketika Anda membutuhkan replay event yang tahan lama, atau ketika Anda memproses jutaan event setiap hari.
Memahami Model Inti Kafka
Arsitektur Kafka dibangun di sekitar topik, partisi, dan consumer group. Topik adalah kategori event — seperti 'order.created' atau 'invoice.paid'. Topik dibagi menjadi partisi, yang merupakan unit paralelisme. Setiap partisi adalah urutan rekaman yang berurutan dan tidak dapat diubah yang disimpan Kafka untuk periode yang dapat dikonfigurasi (default 7 hari). Consumer group memungkinkan beberapa instansi layanan membaca dari topik secara paralel.
Merancang Topik dan Partisi
Desain topik adalah tempat di mana kebanyakan tim membuat kesalahan. Terlalu sedikit partisi membatasi paralelisme — jika Anda memiliki satu partisi, hanya satu consumer yang dapat membacanya sekaligus. Titik awal yang wajar: atur jumlah partisi sama dengan konkurensi consumer puncak yang Anda harapkan. Gunakan kunci partisi yang bermakna — biasanya ID tenant, ID pengguna, atau ID entitas — sehingga event terkait mendarat di partisi yang sama dan mempertahankan urutan untuk entitas tersebut.
Kafka Event-Driven Architecture (NestJS)
Producers (NestJS Services)
┌───────────────────────────┐
│ InvoiceService │──── erp.v1.invoice.created ──┐
│ OrderService │──── erp.v1.order.placed ──┤
│ PaymentService │──── erp.v1.payment.received─┘
└───────────────────────────┘
│
┌────────▼────────┐
│ Kafka Broker │
│ (3 nodes, HA) │
│ │
│ Topic Partitions │
│ P0 | P1 | P2 │
└────────┬────────┘
│
┌───────────────────────────────┼──────────────────────────┐
│ │ │
┌─────▼─────┐ ┌──────▼──────┐ ┌───────▼──────┐
│ Notification│ │ PDF Report │ │ Audit Log │
│ Consumer │ │ Consumer │ │ Consumer │
│ (Group: A) │ │ (Group: B) │ │ (Group: C) │
└────────────┘ └─────────────┘ └──────────────┘
Dead Letter Queue: erp.v1.invoice.created.dlq
(Failed messages routed here for inspection & replay)Dari pengalaman membangun modul event-driven untuk sistem ERP: gunakan nama topik yang deskriptif dan berversi seperti 'erp.v1.invoice.created' dari awal. Mengganti nama topik di produksi memerlukan koordinasi semua produsen dan consumer secara bersamaan — migrasi yang menyakitkan. Sertakan domain, versi, entitas, dan kata kerja event dalam nama, dan Anda akan berterima kasih pada diri sendiri ketika menambahkan skema v2 enam bulan kemudian.
Mengimplementasikan Kafka di NestJS dengan @nestjs/microservices
NestJS memiliki dukungan Kafka kelas pertama melalui paket @nestjs/microservices. Anda mengonfigurasi transport microservice Kafka di aplikasi Anda, kemudian menggunakan dekorator untuk mendefinisikan message handler. Pengaturannya memerlukan tiga bagian: layanan produsen (untuk menerbitkan event), modul consumer (dengan dekorator @EventPattern atau @MessagePattern), dan konfigurasi klien Kafka dengan pengaturan retry dan penanganan error yang tepat.
Penanganan Error dan Dead Letter Queue
Kafka produksi tanpa dead letter queue (DLQ) adalah jebakan. Ketika consumer melempar error yang tidak tertangani, perilaku default mencoba ulang tanpa batas — memblokir partisi tersebut dari maju. Tetapkan kebijakan retry yang dibatasi dan arahkan pesan yang gagal ke topik DLQ (misalnya 'erp.v1.invoice.created.dlq'). Consumer DLQ terpisah dapat memberi peringatan tentang kegagalan, menyimpannya untuk inspeksi manual, dan memutarnya ulang setelah perbaikan bug.
// main.ts — Kafka microservice setup
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
transport: Transport.KAFKA,
options: {
client: {
clientId: 'erp-service',
brokers: ['kafka-1:9092', 'kafka-2:9092'],
},
consumer: {
groupId: 'erp-consumer-group',
retry: { retries: 3 },
},
},
});
await app.listen();
}
// invoice.consumer.ts — Event handler
@Controller()
export class InvoiceConsumer {
@EventPattern('erp.v1.invoice.created')
async handleInvoiceCreated(@Payload() data: InvoiceCreatedEvent) {
try {
await this.pdfService.generate(data.invoiceId);
await this.emailService.sendInvoice(data);
} catch (error) {
// Route to DLQ — never retry indefinitely
await this.dlqService.publish('erp.v1.invoice.created.dlq', data, error);
}
}
}Consumer Group dan Skalabilitas
Consumer group adalah mekanisme skalabilitas horizontal Kafka. Tambahkan instansi baru layanan consumer NestJS Anda, dan Kafka secara otomatis menyeimbangkan ulang partisi di semua instansi. Tangkapannya: selama rebalance, semua consumer dalam grup berhenti memproses sementara partisi ditugaskan ulang. Untuk sistem latensi rendah, jaga waktu rebalance consumer group tetap pendek dengan menggunakan fitur keanggotaan statis.
Kafka memerlukan kluster ZooKeeper atau KRaft, tuning partisi yang tepat, strategi rebalancing consumer group, monitoring, dan schema registry jika Anda ingin evolusi skema. Untuk ERP yang menangani 500 event per hari, antrian berbasis PostgreSQL dengan pg-boss atau antrian Redis BullMQ sederhana memberikan 90% manfaat dengan 10% kompleksitas operasional. Saya menggunakan Kafka hanya ketika beberapa layanan independen harus mengonsumsi aliran event yang sama.
Manajemen Skema dengan Schema Registry
Seiring pertumbuhan sistem event-driven Anda, evolusi skema menjadi masalah paling sulit. Saat event 'order.created' mendapatkan kolom wajib baru, ada jendela waktu di mana produsen lama mengirim event tanpa kolom tersebut dan consumer baru mengharapkannya. Confluent Schema Registry (atau ekuivalen open-source-nya seperti Apicurio) memecahkan ini dengan versi skema dan pemeriksaan kompatibilitas.
Penilaian Jujur Saya: Kapan Memilih Kafka
Gunakan Kafka ketika: (1) beberapa layanan independen perlu mengonsumsi aliran event yang sama; (2) Anda membutuhkan replay event yang tahan lama untuk debugging atau membangun read model; (3) Anda memproses jutaan event setiap hari dan membutuhkan skala horizontal; (4) Anda memiliki tim dengan kapasitas untuk mengoperasikan kluster Kafka. Lewati Kafka ketika: sistem Anda memiliki kurang dari 3 layanan yang mengonsumsi event, throughput Anda di bawah 10K event/hari, atau tim Anda tidak memiliki pengalaman operasional Kafka.
