Empat chapter terakhir menyusun aplikasi secara vertikal: presentasi memanggil service, service memanggil repository. Ada bentuk lain yang memotong arah itu secara horizontal: alih-alih memanggil fungsi lain secara langsung, komponen cukup mengumumkan bahwa suatu kejadian telah terjadi, dan siapa pun yang berminat boleh bereaksi. Node.js membawa pola ini sejak core-nya lewat class EventEmitter — mesin publikasi-langganan (pub/sub) dalam satu proses. Pada chapter ini kita bedah kapan kejadian (event) membantu, kapan justru menyulitkan, dan bagaimana pola ini menjadi fondasi antrean pekerjaan.

Dari Panggilan Langsung ke Kejadian #

Bayangkan modul pembayaran yang harus mengirim kwitansi lewat email, mencatat audit, dan menambah poin loyalitas. Dengan panggilan langsung, fungsi pembayaran memanggil tiga fungsi lain bernama satu per satu. Daftar itu tumbuh setiap kali ada reaksi baru — dan modul pembayaran ikut diedit setiap kali, meskipun proses pembayaran sendiri tidak berubah. Dengan event, fungsi pembayaran hanya mengumumkan PembayaranDiterima; pemilik kwitansi, audit, dan loyalitas masing-masing memasang listener sendiri.

AspekPanggilan langsungEvent
Pemanggil mengenal penerimaYa — nama fungsi tertulis eksplisitTidak — cukup nama kejadian
Menambah reaksi baruMengubah kode pemanggilMenambah listener, pemanggil utuh
Mendapatkan hasilNilai balik tersedia langsungTidak ada — kejadian berjalan satu arah
Membaca alur programTerlihat jelas di call stackTersebar lintas listener

Baris terakhir tabel itu adalah harga yang harus dibayar: event menukar keterbacaan alur dengan kelenturan. Karena itu pola ini paling cocok untuk kejadian domain — fakta yang sudah selesai terjadi (PembayaranDiterima, PesananDikirim) — bukan untuk pertanyaan yang butuh jawaban (“berapa sisa stok?”). Pertanyaan tetap layak dijawab lewat panggilan fungsi biasa.

Demo: Domain Event PembayaranDiterima #

Demo berikut memakai satu EventEmitter sebagai bus kejadian. Dua listener terpasang lebih dulu, listener ketiga dipasang di tengah jalan — dan fungsi terimaPembayaran tidak berubah satu baris pun:

js
import { EventEmitter } from 'node:events';

const WAKTU_DEMO = new Date(Date.UTC(2026, 0, 15, 8, 0, 0));

const bus = new EventEmitter();
const emailTerkirim = [];
const catatanAudit = [];

bus.on('PembayaranDiterima', (peristiwa) => {
  emailTerkirim.push(peristiwa.invoiceId);
  console.log(`[email] Kwitansi ${peristiwa.invoiceId} dikirim ke pelanggan`);
});

bus.on('PembayaranDiterima', (peristiwa) => {
  catatanAudit.push({ invoiceId: peristiwa.invoiceId, waktu: peristiwa.waktu });
});

function terimaPembayaran(bus, { invoiceId, jumlah }) {
  console.log(`[kasir] Invoice ${invoiceId} sebesar Rp${jumlah} tercatat`);
  bus.emit('PembayaranDiterima', { invoiceId, jumlah, waktu: WAKTU_DEMO });
}

terimaPembayaran(bus, { invoiceId: 'INV-001', jumlah: 150000 });
terimaPembayaran(bus, { invoiceId: 'INV-002', jumlah: 75000 });

bus.on('PembayaranDiterima', (peristiwa) => {
  console.log(`[loyalitas] Poin ditambahkan untuk ${peristiwa.invoiceId}`);
});

terimaPembayaran(bus, { invoiceId: 'INV-003', jumlah: 320000 });

console.log('email terkirim :', emailTerkirim.join(', '));
console.log('audit          :', catatanAudit.map((t) => t.invoiceId).join(', '));
console.log('waktu audit    :', catatanAudit[0].waktu.toISOString());
console.log('jumlah listener:', bus.listenerCount('PembayaranDiterima'));

Output yang Dihasilkan:

text
[kasir] Invoice INV-001 sebesar Rp150000 tercatat
[email] Kwitansi INV-001 dikirim ke pelanggan
[kasir] Invoice INV-002 sebesar Rp75000 tercatat
[email] Kwitansi INV-002 dikirim ke pelanggan
[kasir] Invoice INV-003 sebesar Rp320000 tercatat
[email] Kwitansi INV-003 dikirim ke pelanggan
[loyalitas] Poin ditambahkan untuk INV-003
email terkirim : INV-001, INV-002, INV-003
audit          : INV-001, INV-002, INV-003
waktu audit    : 2026-01-15T08:00:00.000Z
jumlah listener: 3

Mari kita bedah komponen penyusunnya:

  1. Baris 1 (import): EventEmitter diimpor dari node:events — core module, tanpa package tambahan. Nama bus dipakai karena object ini menjadi jalur bersama tempat kejadian mengalir.
  2. Baris 3 (WAKTU_DEMO): Waktu dibekukan lewat Date.UTC menjadi 15 Januari 2026 pukul 08:00 UTC, sehingga output identik di komputer mana pun dan di setiap eksekusi. Aplikasi produksi menyuntikkan jam sungguhan — teknik suntik dependensi yang sama dengan rate limiter di Section Security.
  3. Baris 9-16 (dua listener pertama): Method on mendaftarkan listener untuk kejadian bernama PembayaranDiterima. Listener email mencetak baris log; listener audit hanya mengumpulkan catatan ke array tanpa bicara. Keduanya menerima payload kejadian berupa object { invoiceId, jumlah, waktu }.
  4. Baris 18-21 (terimaPembayaran): Fungsi kasir menerima bus sebagai parameter — bukan membacanya dari scope module — lalu memanggil bus.emit dengan nama kejadian dan payload. Fungsi ini tidak mengetahui siapa pun yang mendengarkan; jumlah listener boleh nol atau sepuluh, kode kasir tetap sama.
  5. Baris 23-24 (dua pembayaran pertama): Output menunjukkan pola berselang [kasir] lalu [email] — listener berjalan satu per satu secara sinkron saat emit dipanggil, sebelum emit mengembalikan kendali.
  6. Baris 26-30 (listener ketiga): Listener loyalitas didaftarkan setelah dua pembayaran pertama selesai. Pembayaran ketiga lalu menampilkan tiga baris reaksi — bukti bahwa menambah konsumen baru tidak menyentuh kode produksi kejadian sedikit pun. Listener yang terlambat terdaftar juga tidak menerima kejadian masa lalu; audit tetap berisi tiga invoice karena listener audit memang terpasang sejak awal.
  7. Baris 32-35 (ringkasan): Tiga array dan listenerCount menutup demo dengan bukti akhir: email terkirim untuk tiga invoice, audit mencatat tiga invoice dengan waktu beku yang sama, dan bus kini mengelola tiga listener.

Perilaku EventEmitter yang Perlu Dikenal #

Beberapa perilaku EventEmitter layak dipegang sebelum dipakai serius. Method once mendaftarkan listener yang mencabut dirinya sendiri setelah bereaksi sekali. Method eventNames dan listenerCount memotret keadaan bus, dan emit mengembalikan boolean — true bila ada listener, false bila tidak:

js
import { EventEmitter } from 'node:events';

const bus = new EventEmitter();
bus.on('Aktivitas', () => {});
bus.once('Aktivitas', () => {});

console.log('eventNames          :', bus.eventNames());
console.log('listener Aktivitas  :', bus.listenerCount('Aktivitas'));
console.log('listener TidakAda   :', bus.listenerCount('TidakAda'));
console.log('emit TidakAda       :', bus.emit('TidakAda'));
console.log('emit Aktivitas      :', bus.emit('Aktivitas'));
console.log('listener setelahnya :', bus.listenerCount('Aktivitas'));

Output yang Dihasilkan:

text
eventNames          : [ 'Aktivitas' ]
listener Aktivitas  : 2
listener TidakAda   : 0
emit TidakAda       : false
emit Aktivitas      : true
listener setelahnya : 1

Enam baris output itu meringkas tiga fakta. eventNames() mengembalikan daftar kejadian yang punya listener; listenerCount menghitung listener per nama kejadian, termasuk listener once yang belum terpakai. Nilai balik emit memberi tahu apakah kejadian sampai ke siapa pun — angka listener turun dari 2 ke 1 setelah emit('Aktivitas') karena listener once mencabut dirinya.

Kapan Event Membantu, Kapan Mengganggu #

Panduan praktisnya dapat dirangkum dalam tiga pertanyaan. Apakah reaksi terhadap kejadian boleh datang dari komponen yang berbeda-beda? Event membantu. Apakah pemanggil membutuhkan hasil dari reaksi tersebut? Panggilan langsung lebih tepat. Apakah jumlah reaksi diperkirakan bertambah seiring waktu? Event menambal pertumbuhan itu tanpa menyentuh kode lama. Jawaban “ya” pada pertanyaan pertama dan ketiga, “tidak” pada pertanyaan kedua, adalah wilayah kerja event yang sehat.

Melewati Batas Proses #

EventEmitter hidup di dalam satu proses — ketika proses berhenti, seluruh listener dan kejadian yang belum sempat terkirim ikut berakhir. Kebutuhan dunia nyata sering lebih besar: pengiriman email harus bertahan melewati hidup proses, dicoba ulang saat gagal, dan diproses secara berkelompok. Kebutuhan itu dijawab oleh antrean pekerjaan (job queue) dan message broker — komponen terpisah yang menyimpan kejadian ke penyimpanan tahan lama, lalu menyerahkannya ke worker secara terjadwal.

Arsitekturnya tetap sama dengan demo kita: produsen mengumumkan kejadian, konsumen bereaksi, keduanya tidak saling mengenal. Yang berubah hanyalah media antara keduanya. Proyek Background Job Processor pada bagian akhir Course ini membangun antrean semacam itu di atas Node.js — lengkap dengan percobaan ulang dan pemantauan — sehingga pola pada chapter ini menjadi bekal langsung.

Kopling longgar antar komponen kini ada di tangan kita, baik vertikal (lapisan) maupun horizontal (kejadian). Satu kebutuhan praktis masih menunggu: aplikasi yang sama harus bisa menyala dengan pengaturan berbeda di tiap lingkungan tanpa mengubah kode. Chapter Konfigurasi Aplikasi menuntaskan bagian itu.