Arsitektur event-driven semakin relevan ketika aplikasi tidak lagi cukup hanya mengandalkan pola request-response biasa. Pada sistem notifikasi, audit log, pelacakan aktivitas, dan pembaruan status secara real-time, kita sering membutuhkan aliran data yang bersifat asinkron, tahan gagal, dan dapat dikonsumsi oleh banyak komponen secara independen. Di sinilah Kafka berperan sebagai event backbone, sementara Nuxt 3 dapat berfungsi sebagai frontend modern sekaligus dashboard observasi untuk menampilkan event yang sedang terjadi.

Artikel ini membahas bagaimana backend mem-publish event ke Kafka, bagaimana service consumer memproses event, dan bagaimana Nuxt 3 menyajikan hasilnya ke pengguna melalui Server-Sent Events (SSE) atau WebSocket. Selain itu, kita juga akan membahas pola penting seperti retry, idempotensi, dead-letter queue, audit log, tantangan deployment, dan cara debug ketika event tidak sampai ke UI.

Mengapa Kafka dan Nuxt 3 Cocok untuk Kasus Ini

Kafka dirancang untuk menangani aliran event dalam skala besar dengan model append-only log. Setiap event disimpan di topic, lalu dibaca oleh consumer berdasarkan offset. Pendekatan ini sangat cocok untuk:

  • Notifikasi real-time: misalnya notifikasi transaksi berhasil, tiket support baru, atau perubahan status pesanan.
  • Audit log: semua aktivitas penting dapat dicatat sebagai event yang immutable.
  • Fan-out processing: satu event dapat diproses oleh banyak consumer untuk tujuan berbeda, misalnya notifikasi, analytics, dan audit.
  • Loose coupling: backend utama tidak perlu tahu siapa saja yang akan mengonsumsi event.

Nuxt 3, di sisi lain, cocok sebagai lapisan presentasi karena mendukung rendering modern, server routes, composable, dan integrasi yang cukup fleksibel dengan API real-time. Nuxt 3 bukan pengganti Kafka consumer utama, tetapi sangat efektif sebagai:

  • Frontend aplikasi pengguna akhir.
  • Dashboard observasi untuk melihat event, status pemrosesan, dan error.
  • Lapisan distribusi real-time ke browser melalui SSE atau WebSocket.

Arsitektur Dasar: Publish, Consume, dan Sinkronisasi ke UI

Secara umum, alurnya dapat dibagi menjadi beberapa komponen:

  1. Backend domain service menghasilkan event, misalnya notification.created atau order.status_changed.
  2. Producer Kafka mengirim event ke topic yang sesuai.
  3. Consumer service membaca event, memvalidasi payload, menjalankan bisnis proses lanjutan, lalu menyimpan status ke database atau cache.
  4. Realtime gateway mendorong update ke frontend melalui SSE atau WebSocket.
  5. Nuxt 3 frontend/dashboard menampilkan status notifikasi, riwayat event, retry, dan kegagalan.

Arsitektur sederhananya:

Backend API --publish--> Kafka Topic --consume--> Notification Worker
                                           |
                                           +--> Audit Worker
                                           |
                                           +--> Realtime Gateway --SSE/WS--> Nuxt 3 UI

Poin pentingnya: browser tidak langsung berbicara dengan Kafka. Biasanya ada service perantara yang bertugas mengonsumsi event dan mendistribusikannya ke klien dengan protokol yang lebih cocok untuk web, seperti SSE atau WebSocket.

Desain Event yang Stabil dan Mudah Di-debug

Salah satu kesalahan umum dalam sistem event-driven adalah payload event yang terlalu bebas tanpa kontrak yang jelas. Event sebaiknya memiliki struktur yang konsisten agar mudah diolah, dilacak, dan di-debug.

Contoh payload event notifikasi

{
  "eventId": "evt_01HVX9A9T8Q2",
  "eventType": "notification.created",
  "occurredAt": "2026-03-30T10:15:00Z",
  "source": "billing-service",
  "traceId": "trc_abc123",
  "userId": "user_42",
  "payload": {
    "title": "Pembayaran berhasil",
    "message": "Invoice INV-2026-001 telah dibayar",
    "channel": "in_app",
    "priority": "high"
  }
}

Field seperti eventId, traceId, dan occurredAt sangat penting. eventId membantu idempotensi, traceId mempermudah penelusuran lintas service, dan occurredAt membantu analisis urutan kejadian. Hindari mengirim event tanpa metadata dasar karena akan menyulitkan audit dan investigasi produksi.

Topik dan partition

Jangan membuat satu topic raksasa untuk semua jenis event jika kebutuhan retensi, throughput, dan consumer group berbeda. Beberapa pola yang umum:

  • notifications.events untuk event notifikasi.
  • audit.events untuk audit log.
  • notifications.dlq untuk event gagal permanen.

Jika urutan event per pengguna penting, gunakan key seperti userId saat publish ke Kafka agar event pengguna yang sama cenderung masuk ke partition yang sama dan diproses berurutan di dalam partition tersebut.

Contoh Implementasi Producer dan Consumer

Di sisi backend, producer bertugas mengirim event setelah transaksi bisnis berhasil. Praktik yang baik adalah menggunakan pola transactional outbox jika Anda ingin menghindari kondisi ketika data bisnis tersimpan tetapi event gagal terkirim. Namun untuk contoh sederhana, berikut bentuk producer menggunakan ekosistem Node.js dan Kafka client seperti KafkaJS.

Producer event dari backend

import { Kafka } from 'kafkajs'

const kafka = new Kafka({
  clientId: 'billing-service',
  brokers: [process.env.KAFKA_BROKER || 'localhost:9092']
})

const producer = kafka.producer()

export async function publishNotificationCreated(data: {
  userId: string,
  title: string,
  message: string,
  traceId: string
}) {
  await producer.connect()

  const event = {
    eventId: crypto.randomUUID(),
    eventType: 'notification.created',
    occurredAt: new Date().toISOString(),
    source: 'billing-service',
    traceId: data.traceId,
    userId: data.userId,
    payload: {
      title: data.title,
      message: data.message,
      channel: 'in_app',
      priority: 'high'
    }
  }

  await producer.send({
    topic: 'notifications.events',
    messages: [
      {
        key: data.userId,
        value: JSON.stringify(event)
      }
    ]
  })
}

Dalam aplikasi nyata, koneksi producer sebaiknya dikelola sebagai singleton dan tidak dibuat-putus pada setiap request. Jika event diterbitkan sangat sering, pola ini lebih efisien dan stabil.

Consumer untuk menyimpan notifikasi dan meneruskan ke gateway real-time

import { Kafka } from 'kafkajs'

const kafka = new Kafka({
  clientId: 'notification-worker',
  brokers: [process.env.KAFKA_BROKER || 'localhost:9092']
})

const consumer = kafka.consumer({ groupId: 'notification-worker-group' })

const processedEventIds = new Set()

async function saveNotification(event: any) {
  if (processedEventIds.has(event.eventId)) return

  processedEventIds.add(event.eventId)
  // simpan ke database
  // push ke realtime gateway
}

export async function startConsumer() {
  await consumer.connect()
  await consumer.subscribe({ topic: 'notifications.events', fromBeginning: false })

  await consumer.run({
    eachMessage: async ({ message }) => {
      if (!message.value) return
      const event = JSON.parse(message.value.toString())
      await saveNotification(event)
    }
  })
}

Contoh di atas menyederhanakan idempotensi menggunakan Set, tetapi di produksi Anda perlu menyimpan eventId di database atau Redis agar tahan restart. Jangan mengandalkan memori proses jika Anda butuh jaminan lebih kuat.

Peran Nuxt 3: Frontend Aplikasi dan Dashboard Observasi

Nuxt 3 sebaiknya tidak bertugas sebagai consumer utama Kafka karena tanggung jawab tersebut lebih cocok untuk worker/backend service. Namun Nuxt 3 sangat baik untuk dua hal:

  • UI notifikasi pengguna, misalnya panel notifikasi di aplikasi.
  • Dashboard observasi, misalnya menampilkan daftar event terbaru, status retry, jumlah event gagal, dan isi dead-letter queue.

Pola yang umum adalah Nuxt 3 berkomunikasi dengan realtime gateway atau API internal yang sudah menerima hasil pemrosesan dari consumer Kafka.

SSE di Nuxt 3 untuk update satu arah

Jika kebutuhan Anda hanya server ke browser, SSE biasanya lebih sederhana dibanding WebSocket. Cocok untuk feed notifikasi, status job, dan audit stream ringan.

export function useNotificationStream() {
  const notifications = useState('notifications', () => [])

  const connect = () => {
    const es = new EventSource('/api/realtime/notifications')

    es.onmessage = (event) => {
      const data = JSON.parse(event.data)
      notifications.value.unshift(data)
    }

    es.onerror = () => {
      es.close()
      setTimeout(connect, 3000)
    }
  }

  return { notifications, connect }
}

Kelebihan SSE adalah implementasi lebih sederhana, mendukung reconnect otomatis di banyak browser, dan cukup efisien untuk stream teks. Kelemahannya: satu arah, tidak cocok jika klien perlu sering mengirim pesan balik real-time ke server.

WebSocket untuk interaksi dua arah

Jika dashboard observasi membutuhkan filter real-time, subscribe dinamis per channel, atau kontrol interaktif yang lebih kompleks, WebSocket bisa lebih tepat. Namun Anda perlu memikirkan autentikasi koneksi, heartbeat, manajemen koneksi idle, dan skalabilitas horizontal dengan lebih serius.

Pemilihan praktis:

  • Pilih SSE jika hanya butuh push notifikasi atau feed event ke browser.
  • Pilih WebSocket jika butuh komunikasi dua arah, room, atau interaksi real-time yang lebih kaya.

Retry, Idempotensi, dan Dead-Letter Queue

Sistem event-driven yang sehat tidak hanya memproses event ketika semuanya normal, tetapi juga memiliki strategi ketika terjadi kegagalan parsial.

Retry

Retry diperlukan ketika kegagalan bersifat sementara, misalnya database timeout atau service downstream tidak tersedia. Hindari retry tanpa batas karena bisa memperparah beban sistem. Gunakan pendekatan seperti:

  • Exponential backoff untuk menjarangkan percobaan ulang.
  • Retry topic terpisah, misalnya notifications.retry.1m.
  • Maksimum retry yang jelas sebelum event dipindah ke DLQ.

Idempotensi

Kafka memberi jaminan pengiriman yang kuat, tetapi consumer tetap bisa memproses event lebih dari sekali dalam kondisi tertentu, misalnya saat rebalance atau restart. Karena itu, handler harus idempoten. Contoh praktik:

  • Simpan eventId yang sudah diproses.
  • Gunakan constraint unik di database.
  • Pastikan operasi seperti insert notifikasi tidak menghasilkan duplikasi bila event sama diproses ulang.

Dead-letter queue

DLQ digunakan untuk event yang gagal diproses secara permanen, misalnya payload rusak atau validasi bisnis tidak terpenuhi. Jangan buang event begitu saja. Simpan ke topic DLQ lengkap dengan alasan kegagalannya agar bisa diperiksa dan direplay bila perlu.

{
  "originalTopic": "notifications.events",
  "failedAt": "2026-03-30T10:20:00Z",
  "reason": "validation_error: missing userId",
  "event": { "eventId": "evt_01HVX9A9T8Q2" }
}

Pada dashboard Nuxt 3, data DLQ ini bisa ditampilkan sebagai tabel investigasi agar tim operasional cepat mengetahui pola kegagalan.

Audit Log dan Observability

Audit log berbeda dari notifikasi biasa. Audit log lebih menekankan jejak perubahan: siapa melakukan apa, kapan, dan dari mana. Event audit sebaiknya immutable dan tidak diubah setelah ditulis. Kafka cocok untuk menyimpan aliran audit sebelum diteruskan ke storage analitik atau database pencarian.

Untuk observability, minimal Anda perlu:

  • Trace ID yang konsisten dari request awal hingga event consumer.
  • Structured logging agar log mudah dicari berdasarkan traceId, eventId, dan topic.
  • Lag monitoring consumer group untuk mendeteksi backlog.
  • Metrics seperti jumlah event masuk, sukses, gagal, retry, dan DLQ.

Nuxt 3 sebagai dashboard observasi dapat menampilkan metrik ini lewat API backend. Jangan menjadikan frontend sebagai sumber kebenaran observability; frontend hanya menyajikan data dari sistem monitoring atau service internal.

Struktur Proyek yang Disarankan

Pisahkan tanggung jawab agar kode tetap mudah dirawat. Salah satu struktur yang cukup masuk akal:

apps/
  nuxt-dashboard/
    pages/
    components/
    composables/
    server/api/
  backend-api/
    src/events/
    src/modules/
  notification-worker/
    src/consumers/
    src/retries/
    src/dlq/
  realtime-gateway/
    src/sse/
    src/websocket/
packages/
  shared-event-schema/
  shared-logger/
  shared-config/

Dengan struktur ini, skema event dapat dibagikan lintas service sehingga risiko mismatch payload berkurang. Ini sangat membantu ketika tim frontend dan backend berkembang secara paralel.

Tantangan Deployment di Produksi

Deployment sistem seperti ini memiliki beberapa tantangan yang sering diremehkan:

Koneksi long-lived

SSE dan WebSocket membutuhkan koneksi panjang. Jika Anda berada di belakang reverse proxy atau load balancer, pastikan timeout, keep-alive, dan buffer dikonfigurasi dengan benar. Salah konfigurasi sering membuat koneksi terputus diam-diam.

Scaling horizontal

Jika realtime gateway di-scale ke banyak instance, Anda perlu memastikan event dapat disebarkan ke instance yang menangani koneksi user tertentu. Biasanya ini membutuhkan Redis pub/sub, NATS, atau broker internal lain untuk fan-out antar instance gateway.

Kafka di environment container

Masalah umum saat menggunakan Docker atau Kubernetes adalah konfigurasi advertised.listeners Kafka yang salah, sehingga producer atau consumer bisa connect ke broker awal tetapi gagal saat metadata cluster dikembalikan. Ini sering terlihat seperti “koneksi berhasil, tetapi publish timeout”.

Debugging Event yang Tidak Sampai

Ketika notifikasi tidak muncul di Nuxt 3, jangan langsung menyalahkan frontend. Telusuri alurnya secara sistematis:

  1. Cek producer: apakah event benar-benar dikirim ke topic? Periksa log publish dan error broker.
  2. Cek topic: apakah event masuk ke Kafka? Gunakan tool CLI atau UI seperti Kafka UI untuk inspeksi message.
  3. Cek consumer group: apakah consumer aktif, lag bertambah, atau offset macet?
  4. Cek logika consumer: apakah event gagal validasi, terkena retry, atau masuk DLQ?
  5. Cek storage: apakah data notifikasi tersimpan di database?
  6. Cek realtime gateway: apakah event berhasil diteruskan ke channel user yang tepat?
  7. Cek browser: apakah koneksi SSE/WebSocket aktif, ada error CORS, auth expired, atau reconnect loop?

Beberapa tips praktis:

  • Selalu log eventId dan traceId di semua service.
  • Buat endpoint observasi internal untuk melihat status consumer dan offset terakhir.
  • Tampilkan event gagal dan DLQ di dashboard agar investigasi tidak bergantung pada akses shell ke server.
  • Jika menggunakan SSE, periksa apakah proxy men-buffer response sehingga event tidak segera terkirim.

Catatan: kegagalan real-time sering bukan karena Kafka, melainkan karena lapisan setelah Kafka: consumer crash, gateway tidak sinkron, token auth kadaluarsa, atau koneksi browser diputus proxy.

Penutup

Menggabungkan Kafka dan Nuxt 3 untuk notifikasi real-time adalah pendekatan yang kuat jika Anda membutuhkan pemrosesan event yang asinkron, dapat diobservasi, dan mudah dikembangkan ke banyak consumer. Kafka menangani distribusi event dan ketahanan aliran data, sementara Nuxt 3 menyediakan antarmuka pengguna dan dashboard observasi yang nyaman untuk tim operasional maupun pengguna akhir.

Kunci keberhasilan implementasi ini bukan hanya pada kemampuan publish-consume event, tetapi pada disiplin desain event, idempotensi, retry yang terkendali, dead-letter queue, serta observability yang baik. Jika fondasi ini dibangun dengan benar, sistem akan lebih mudah di-debug, lebih tahan terhadap kegagalan parsial, dan lebih aman untuk dikembangkan seiring pertumbuhan kebutuhan bisnis.