L
Node.js • Literally Web ID

Arsitektur Microservices Real-Time dengan Node.js, NestJS, dan Apache Kafka

⏱️ 18 menit baca • 📅 16 Oct 2026 • 👁️ 1 views
Arsitektur Microservices Real-Time dengan Node.js, NestJS, dan Apache Kafka

1. Konteks Industri & Analisis Akar Masalah

Dalam lanskap pengembangan sistem web skala enterprise modern, transisi dari arsitektur monolitik menuju Microservices bukanlah sekadar tren, melainkan sebuah keharusan bisnis untuk mencapai skalabilitas horizontal dan isolasi kegagalan (fault isolation). Namun, seiring bertambahnya jumlah layanan, pola komunikasi tradisional berbasis HTTP REST API sinkron (request-response) mulai menunjukkan batas performanya.

Bottleneck Performa pada Komunikasi Sinkron Tradisional

Pendekatan REST API atau gRPC sinkron dalam ekosistem microservices sering kali berujung pada apa yang disebut sebagai Cascading Failures dan Thread Exhaustion. Ketika Layanan A memanggil Layanan B, dan Layanan B memanggil Layanan C, utas (thread) pada Layanan A akan tertahan (blocked) menunggu respons dari B dan C.

  • Latensi Kumulatif: Total latensi adalah penjumlahan dari seluruh latensi jaringan layanan yang terlibat ($L_{total} = L_A + L_B + L_C$).
  • Titik Kegagalan Tunggal (SPOF): Jika Layanan C mengalami lonjakan trafik dan melambat, antrean permintaan akan menumpuk mundur hingga ke Layanan A, mengakibatkan seluruh sistem kolaps meskipun Layanan A dan B secara lokal memiliki kapasitas sumber daya yang memadai.
  • Kurangnya Retensi Pesan: HTTP bersifat ephemeral. Jika penerima sedang down saat pengiriman data, pesan tersebut hilang kecuali pemanggil menerapkan mekanisme retry manual yang kompleks.

Kondisi Sebelum vs Sesudah Penerapan Event-Driven Architecture (EDA) dengan Apache Kafka

Parameter Sebelum (HTTP REST / RPC Sinkron) Sesudah (Event-Driven dengan Apache Kafka & NestJS)
Coupling Kuat (Tight), layanan harus saling mengetahui alamat IP/endpoint. Longgar (Loose), produsen dan konsumen hanya perlu tahu nama Topic.
Availability Bergantung pada ketersediaan seluruh layanan secara simultan. Tinggi. Produsen tetap bisa mengirim pesan meskipun konsumen sedang offline.
Throughput Rendah hingga Sedang, dibatasi oleh kapasitas blokir utas jaringan. Sangat Tinggi, didukung model zero-copy data transfer dari Kafka.
Backpressure Sulit dikelola, sering memicu Out of Memory (OOM) pada server. Dikelola secara native oleh Kafka Consumer Group dan partition offset.

2. Konsep Inti & Deep Dive Arsitektur

Apache Kafka adalah platform distributed event streaming yang dirancang untuk menangani jutaan pesan per detik dengan latensi rendah. Berbeda dari message broker tradisional (seperti RabbitMQ) yang berfokus pada antrean tugas (task queues), Kafka berfokus pada commit log yang tidak dapat diubah (append-only immutable commit log).

Alur Eksekusi Data (Under the Hood)

+-----------------------+
|  NestJS Microservice  |
|     (Producer)        |
+-----------------------+
            |
            | 1. emit('order.created', payload)
            v
+-------------------------------------------------------+
|                     Apache Kafka                      | 
|  +-------------------------------------------------+  |
|  | Topic: orders-topic                             |  |
|  |  +-------------+  +-------------+  +----------+ |  |
|  |  | Partition 0 |  | Partition 1 |  | Part 2   | |  |
|  |  +-------------+  +-------------+  +----------+ |  |
|  +-------------------------------------------------+  |
+-------------------------------------------------------+
            |                                 ^
            | 2. Poll Records                 | 3. Ack (Commit Offset)
            v                                 |
+-------------------------------------------------------+
|                 NestJS Microservice                   | 
|                   (Consumer Group)                    | 
+-------------------------------------------------------+
  1. Producer (NestJS): Mengirimkan event ke Kafka broker menggunakan protokol TCP biner kustom. Event dikelompokkan ke dalam Topic dan didistribusikan ke Partition tertentu berdasarkan Key (untuk menjaga urutan peristiwa per entitas).
  2. Broker & Storage: Kafka menyimpan pesan secara berurutan di disk dengan struktur data append-only log. Memanfaatkan page cache sistem operasi dan teknik sendfile (zero-copy), Kafka menghindari pembacaan memori berlebih.
  3. Consumer (NestJS): Mengambil (pull) data dari partisi secara asinkron. Konsumen dikelompokkan dalam Consumer Group untuk memastikan pembagian beban kerja otomatis. Setelah event diproses sukses, konsumen melakukan commit offset untuk menandai posisi pembacaan.

3. Perbandingan Komparatif / Analisis Pendekatan

Dalam membangun sistem real-time dengan Node.js, pemilihan arsitektur transport message broker sangat menentukan stabilitas jangka panjang. Berikut adalah perbandingan mendalam antara Apache Kafka, RabbitMQ, dan Redis Pub/Sub.

Fitur Arsitektur Apache Kafka RabbitMQ (AMQP) Redis Pub/Sub
Model Penyimpanan Disk-based (Persistent, configurable retention) Memori & Disk (Opsional, dihapus setelah dikonsumsi) Memori Murni (Volatile, pesan hilang jika koneksi putus)
Throughput & Skala Ratusan ribu hingga jutaan pesan/detik Puluhan ribu pesan/detik Sangat tinggi, namun terbatas RAM server
Replay Events Sangat Didukung (bisa membaca ulang history log) Tidak didukung (kecuali via plugin dead-letter) Tidak didukung sama sekali
Konsumsi Memori RAM Rendah (Sangat bergantung pada Page Cache OS) Sedang (Tergantung ukuran antrean pesan aktif) Tinggi (Semua pesan berada di RAM)
Kompleksitas Operasional Tinggi (Membutuhkan ZooKeeper / KRaft quorum) Sedang Rendah (Mudah di-setup, rentan data loss)
Penggunaan Ideal Event Sourcing, Log Aggregation, Real-time Streaming Task Queue, Kompleks Routing Router Real-time chat sederhana, Cache invalidation transient

[!WARNING] Jangan pernah menggunakan Redis Pub/Sub untuk arsitektur financial transactions atau critical event logging di mana kehilangan satu pesan pun dapat menyebabkan inkonsistensi data serius.


4. Prasyarat Sistem & Persiapan Lingkungan

Sebelum menyelami kode sumber, pastikan infrastruktur lokal atau staging Anda memenuhi spesifikasi berikut:

  • Node.js: Versi 20.x LTS atau lebih baru.
  • NestJS CLI: Versi 10.x.
  • Docker & Docker Compose: Untuk menjalankan instance Apache Kafka dan Zookeeper secara terisolasi.
  • RAM: Minimal 4GB dialokasikan untuk mesin pengembang.

File Konfigurasi Infrastruktur (docker-compose.yml)

Buat file konfigurasi Docker Compose untuk menjalankan Kafka menggunakan mode KRaft (tanpa Zookeeper untuk efisiensi modern):

version: '3.8'

services:
  kafka:
    image: confluentinc/cp-kafka:7.5.0
    container_name: enterprise-kafka
    ports:
      - "9092:9092"
    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'
      CLUSTER_ID: 'Mk3OEYBSD3T24T8xjSKsWQ'
    volumes:
      - kafka_data:/tmp/kraft-combined-logs

volumes:
  kafka_data:
    driver: local

Jalankan layanan infrastruktur dengan perintah terminal:

docker-compose up -d

5. Panduan Implementasi Langkah demi Langkah

Kita akan membangun dua modul utama dalam monorepo NestJS atau dua layanan terpisah: Order Producer Service dan Inventory Consumer Service.

Langkah 1: Inisialisasi Proyek NestJS & Instalasi Dependency

nest new order-service
cd order-service
npm i --save @nestjs/microservices kafkajs

Langkah 2: Konfigurasi Main Application untuk Kafka Transport (src/main.ts)

Konfigurasikan NestJS agar bertindak sebagai microservice hybrid (bisa menerima HTTP dan terhubung ke Kafka secara bersamaan).

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Transport, MicroserviceOptions } from '@nestjs/microservices';

async function bootstrap() {
  // Membuat HTTP Application utama
  const app = await NestFactory.create(AppModule);

  // Menghubungkan NestJS ke Kafka Broker sebagai Microservice tambahan
  app.connectMicroservice<MicroserviceOptions>({
    transport: Transport.KAFKA,
    options: {
      client: {
        brokers: ['localhost:9092'],
        clientId: 'order-producer',
      },
      consumer: {
        groupId: 'order-service-group',
        allowAutoTopicCreation: true,
      },
    },
  });

  await app.startAllMicroservices();
  await app.listen(3000);
  console.log(`Order Service running on port 3000 and connected to Kafka`);
}
bootstrap();

Langkah 3: Implementasi Order Controller & Producer (src/order.controller.ts)

import { Controller, Post, Body, Inject, OnModuleInit } from '@nestjs/common';
import { ClientKafka } from '@nestjs/microservices';
import { Observable } from 'rxjs';

@Controller('orders')
export class OrderController implements OnModuleInit {
  constructor(
    @Inject('KAFKA_SERVICE') private readonly kafkaClient: ClientKafka,
  ) {}

  async onModuleInit() {
    // Subscribe ke topic balasan jika menggunakan pattern request-response
    this.kafkaClient.subscribeToResponseOf('order.created');
    await this.kafkaClient.connect();
  }

  @Post()
  createOrder(@Body() orderData: { productId: string; quantity: number; userId: string }) {
    const eventPayload = {
      orderId: `ORD-${Date.now()}`,
      ...orderData,
      timestamp: new Date().toISOString(),
    };

    // Mengirim event secara asinkron tanpa menunggu respons (Fire and Forget)
    this.kafkaClient.emit('order.created', eventPayload);

    return {
      status: 'success',
      message: 'Order received and dispatching event to Kafka',
      data: eventPayload,
    };
  }
}

Langkah 4: Modul Konfigurasi Kafka Client (src/app.module.ts)

import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { OrderController } from './order.controller';

@Module({
  imports: [
    ClientsModule.register([
      {[
        name: 'KAFKA_SERVICE',
        transport: Transport.KAFKA,
        options: {
          client: {
            brokers: ['localhost:9092'],
            clientId: 'order-producer-client',
          },
          producer: {
            allowAutoTopicCreation: true,
            // Memastikan retries dan idempotency tingkat lanjut
            idempotent: true,
          },
        },
      }],
    ]),
  ],
  controllers: [OrderController],
})
vport class AppModule {}

Langkah 5: Pembuatan Inventory Service (Consumer Microservice)

Buat proyek terpisah atau modul baru untuk memproses event order.created:

nest new inventory-service
cd inventory-service
npm i --save @nestjs/microservices kafkajs

Konfigurasi main.ts khusus sebagai Kafka Consumer worker:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Transport, MicroserviceOptions } from '@nestjs/microservices';

async function bootstrap() {
  const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
    transport: Transport.KAFKA,
    options: {
      client: {
        brokers: ['localhost:9092'],
        clientId: 'inventory-consumer',
      },
      consumer: {
        groupId: 'inventory-processor-group',
        // Mengatur commit manual untuk fault tolerance tingkat tinggi
        autoCommit: false,
      },
    },
  });

  await app.listen();
  console.log('Inventory Microservice is listening to Kafka events...');
}
bootstrap();

Implementasi Listener pada Controller (src/inventory.controller.ts):

import { Controller } from '@nestjs/common';
import { EventPattern, Payload, Ctx, KafkaContext } from '@nestjs/microservices';

@Controller()
export class InventoryController {
  @EventPattern('order.created')
  async handleOrderCreated(@Payload() data: any, @Ctx() context: KafkaContext) {
    const channel = context.getChannelRef();
    const offset = context.getMessage().offset;
    const partition = context.getPartition();
    const topic = context.getTopic();

    console.log(`Processing Order ID: ${data.orderId} from Topic: ${topic}, Partition: ${partition}, Offset: ${offset}`);

    try {
      // Simulasi pengurangan stok database
      await this.decreaseStock(data.productId, data.quantity);

      // Manual commit offset setelah sukses diproses
      await channel.commitOffsets([
        {
          topic,
          partition,
          offset: (Number(offset) + 1).toString(),
        },
      ]);

      console.log(`Successfully processed and committed offset for Order: ${data.orderId}`);
    } catch (error) {
      console.error(`Failed to process order ${data.orderId}:`, error.message);
      // Di sini Anda bisa menerapkan mekanisme Dead Letter Queue (DLQ)
    }
  }

  private async decreaseStock(productId: string, quantity: number): Promise<void> {
    // Logika bisnis akses database (MySQL/PostgreSQL)
    return new Promise((resolve) => setTimeout(resolve, 500));
  }
}

6. Bedah Kode & Penjelasan Detail Baris Kunci

  • idempotent: true pada Producer: Properti ini sangat vital dalam sistem keuangan atau e-commerce. Jika terjadi gangguan jaringan sesaat setelah produsen mengirim pesan, Kafka dapat mengirim ulang tanpa menciptakan duplikasi data pada partisi log.
  • autoCommit: false & channel.commitOffsets(): Secara default, Kafka consumer melakukan auto-commit offset secara periodik. Jika proses aplikasi crash tepat setelah data ditarik dari broker namun sebelum sempat diproses di database, data tersebut akan hilang dari sudut pandang pemrosesan. Dengan mematikan auto-commit dan melakukannya secara manual hanya setelah sukses, kita menjamin At-Least-Once Delivery Semantics.
  • KafkaContext: Memungkinkan pengembang mengakses metadata tingkat rendah seperti nomor partisi, offset spesifik, dan referensi broker untuk keperluan distributed tracing dan audit logging.

7. Skenario Produksi & Studi Kasus Dunia Nyata

Studi Kasus: Flash Sale E-Commerce dengan 50.000 Request/Detik

Ketika platform e-commerce mengadakan flash sale, lonjakan trafik HTTP secara bersamaan dapat melumpuhkan database utama jika semua request langsung melakukan SQL INSERT.

Arsitektur Solusi:

  1. API Gateway (Nginx/NestJS) memvalidasi token JWT pengguna secara cepat, lalu langsung melakukan emit event ke Kafka dan mengembalikan respons HTTP 202 Accepted (Queued) dalam waktu $< 15$ milidetik.
  2. Kafka Partitioning Strategy: Pesan dikelompokkan berdasarkan productId sebagai Partition Key. Hal ini memastikan seluruh pesanan untuk produk yang sama masuk ke partisi dan antrean yang sama, menjaga integritas stok tanpa race condition lintas thread.
  3. Worker Pool (Inventory Microservice): Sejumlah instance NestJS membaca pesan dari partisi Kafka secara paralel, memproses pengurangan stok dengan aman menggunakan transaksi Redis/MySQL Optimistic Locking.

8. Common Pitfalls, Anti-Patterns & Solusi

  1. Anti-Pattern: Mengabaikan Partisi Berdasarkan Key
    • Masalah: Mengirim event tanpa Partition Key menyebabkan Kafka mendistribusikan pesan secara Round-Robin acak. Akibatnya, urutan kejadian (event ordering) rusak (misalnya event pembayaran datang lebih dulu sebelum event pembuatan pesanan).
    • Solusi: Selalu tentukan kunci unik entitas (seperti orderId atau userId) saat mengirim event.
  2. Pitfall: Pemrosesan Berat di dalam Event Handler (Blocking Event Loop)
    • Masalah: Melakukan operasi CPU-bound yang intensif (seperti enkripsi berat atau manipulasi gambar besar) di dalam thread consumer NestJS akan menghentikan heartbeat Kafka.
    • Solusi: Gunakan Worker Threads atau delegasikan tugas berat ke Background Queue terpisah (seperti BullMQ berbasis Redis) setelah event diterima dari Kafka.
  3. Kesalahan Konfigurasi session.timeout.ms
    • Masalah: Jika proses validasi memakan waktu lebih lama dari batas waktu session timeout, broker mengira consumer telah mati dan memicu rebalancing cluster secara terus-menerus (rebalance storm).
    • Solusi: Sesuaikan parameter session.timeout.ms dan heartbeat.interval.ms dengan beban kerja realistis aplikasi Anda.

9. Benchmark, Metrik Kinerja & Tuning Performa

Untuk menguji ketahanan sistem asinkron yang telah dibangun, gunakan tool load testing berbasis Go seperti k6.

Skrip Uji Beban k6 (load-test.js)

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '30s', target: 1000 }, // Ramp up ke 1000 VU
    { duration: '1m', target: 5000 },  // Pertahankan beban tinggi
    { duration: '10s', target: 0 },    // Ramp down
  ],
};

export default function () {
  const url = 'http://localhost:3000/orders';
  const payload = JSON.stringify({
    productId: 'PROD-XYZ-99',
    quantity: Math.floor(Math.random() * 3) + 1,
    userId: `user-${Math.floor(Math.random() * 100000)}`,
  });

  const params = {
    headers: { 'Content-Type': 'application/json' },
  };

  const res = http.post(url, payload, params);
  check(res, {
    'status is 201 or 202': (r) => r.status === 201 || r.status === 202,
    'response time < 50ms': (r) => r.timings.duration < 50,
  });
  sleep(0.01);
}

Tuning Parameter Sistem Operasi Linux untuk Kafka/Node.js

Ubah konfigurasi kernel Linux di /etc/sysctl.conf untuk memaksimalkan kapasitas soket jaringan:

# Meningkatkan batas maksimal file descriptor
fs.file-max = 2097152

# Optimasi buffer TCP jaringan
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216

10. Checklist Penerapan Produksi & Kesimpulan

Sebelum meluncurkan arsitektur microservices berbasis NestJS dan Apache Kafka ke lingkungan Production (AWS/GCP/Kubernetes), pastikan checklist operasional berikut terpenuhi:

  • Replikasi Kafka: Pastikan replication factor minimal bernilai 3 untuk topik penting guna mencegah kehilangan data saat node broker mati.
  • Monitoring & Alerting: Integrasikan metrik Prometheus dan Grafana untuk memantau Consumer Lag secara real-time.
  • Graceful Shutdown: Implementasikan penanganan sinyal SIGTERM di NestJS agar konsumen dapat menyelesaikan pemrosesan pesan aktif sebelum proses dihentikan (drain connection).
  • Security Layer: Aktifkan enkripsi TLS dan autentikasi SASL/SCRAM pada koneksi klien Kafka.
  • Dead Letter Queue (DLQ): Sediakan mekanisme penampungan pesan gagal (poison messages) agar tidak memblokir antrean utama.

Penerapan arsitektur event-driven dengan Apache Kafka dan NestJS memberikan fondasi tangguh bagi sistem enterprise yang membutuhkan skalabilitas ekstrem, ketersediaan tinggi, dan pemisahan tanggung jawab layanan yang bersih.

🏷️ Tags: #Microservices #Node.js #Apache Kafka #Event-Driven Architecture #Nestjs