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? Saya telah membangun sistem ERP di Commsult Indonesia yang menggunakan antrian berbasis database sederhana untuk pemicu email dan pembuatan invoice — 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.
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.
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 ERP kami di Commsult: 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.
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.
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 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.
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.
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.