Pub/Sub Pattern — Design Patterns

Pub/Sub (Publish/Subscribe) adalah variasi Observer Pattern dimana publisher dan subscriber tidak saling kenal. Komunikasi terjadi melalui perantara (message…

Pub/Sub (publish/subscribe) adalah cara mengirim kabar lewat perantara. Pengirim (publisher) mengirim pesan ke sebuah topik di perantara yang disebut broker, dan penerima (subscriber) berlangganan topik itu di broker. Publisher dan subscriber gak saling kenal: yang mereka sama-sama tahu cuma broker dan nama topiknya. Bayangin siaran radio. Stasiun memancarkan di satu frekuensi, dan siapa pun yang menyetel frekuensi itu ikut mendengar. Stasiunnya gak tahu siapa pendengarnya, dan pendengarnya gak perlu tahu letak studionya. Tapi kalau radiomu mati saat berita dibacakan, berita itu lewat begitu saja.

🧪 Tebakan cepat: gudang Kurir Kancil mem-publish 3 pesan "paket transit". Layanan notifikasi baru subscribe sesudah pesan kedua, karena servernya baru selesai restart. Berapa SMS yang terkirim? Tebak dulu, jawabannya ada di bagian "Pesan hilang kalau belum ada yang subscribe".

Masalahnya: gudang harus kenal semua penerima

Di lesson Observer, tracker dan listener-nya hidup di satu program. Sekarang gudang punya layanan sendiri, terpisah dari layanan notifikasi dan laporan. Versi pertamanya memanggil mereka satu per satu:

// gudang.js versi pertama
function paketMasukGudang(resi, kota) {
  notifikasi.kirimSms(resi, kota) // gudang harus kenal notifikasi
  laporan.tambah(kota)            // ...dan laporan, dan tiap layanan baru
}

Layanan tagihan mau ikut tahu? Kode gudang diubah lagi. Layanan notifikasi sedang mati? Panggilan pertama gagal, dan pemindaian paket di gudang ikut berhenti.

Broker di tengah: publish dan subscribe

const broker = {
  listeners: {}, // { "paket.status": [fn, fn] }
  subscribe(topic, fn) {
    (this.listeners[topic] ??= []).push(fn)
    return () => {
      this.listeners[topic] = this.listeners[topic].filter((f) => f !== fn)
    }
  },
  publish(topic, message) {
    const fns = this.listeners[topic] ?? []
    fns.forEach((fn) => fn(message))
    return fns.length // berapa subscriber yang menerima
  },
}

// gudang.js: cuma kenal broker dan nama topik
function paketMasukGudang(resi, kota) {
  return broker.publish("paket.status", { resi, status: "transit", kota })
}

// notifikasi.js: cuma kenal broker dan nama topik
broker.subscribe("paket.status", (m) => console.log("SMS: " + m.resi + " transit di " + m.kota))

console.log(paketMasukGudang("KCL-0003", "Medan"))
// SMS: KCL-0003 transit di Medan
// 1

Banyak library menamai method-nya on dan emit. Bentuknya sama: emit membaca bus.listeners[event] ?? [], lalu memanggil tiap function di dalamnya dengan datanya.

Bedanya dengan Observer di lesson 3

Observer (tracker) Pub/Sub
yang memegang daftar penerima subject itu sendiri broker
yang harus dikenal penerima object tracker broker dan nama topik
tempat jalannya satu program bisa beda program, beda server

Di dalam satu program, broker cuma object biasa, mirip EventEmitter. Browser bahkan punya bus bawaan: const bus = new EventTarget(), lalu bus.addEventListener("paket.status", fn) dan bus.dispatchEvent(new CustomEvent("paket.status", { detail: pesan })). Bedanya baru terasa saat publisher dan subscriber ada di program berbeda: broker-nya jadi server sendiri, seperti Redis Pub/Sub, RabbitMQ, atau Kafka. Antar-tab browser ada BroadcastChannel.

Pesan hilang kalau belum ada yang subscribe

Broker pub/sub cuma meneruskan pesan ke subscriber yang ada saat itu. Dia gak menyimpan apa pun.

pesan subscriber saat itu publish mengembalikan SMS
KCL-0001 Bandung belum ada 0 gak ada
KCL-0002 Surabaya belum ada 0 gak ada
notifikasi subscribe
KCL-0003 Medan notifikasi 1 SMS: KCL-0003 transit di Medan

Cuma 1 SMS, untuk pesan ketiga. Itu jawaban tebakannya. Dua pesan pertama hilang tanpa error. Redis sama: PUBLISH mengembalikan jumlah penerima, (integer) 0 kalau gak ada, dan pesannya hilang. Tab yang dibuka belakangan juga gak menerima pesan BroadcastChannel yang dikirim sebelumnya. Nama topik yang salah ketik, misalnya "paket.stauts", berakhir sama: 0 penerima, tanpa error.

Antrian menyimpan pesan sampai diambil

Kalau pesan gak boleh hilang, pakai antrian (queue): tiap layanan punya antrian sendiri yang dibuat sejak awal, dan pesan menunggu di situ sampai diambil.

const antrian = { notifikasi: [] } // dibuat sejak awal

function publishKeAntrian(message) {
  antrian.notifikasi.push(message) // disimpan sampai diambil
}

publishKeAntrian({ resi: "KCL-0001", kota: "Bandung" })
publishKeAntrian({ resi: "KCL-0002", kota: "Surabaya" })

// notifikasi menyala, lalu mengambil yang menunggu, dari yang paling lama
while (antrian.notifikasi.length > 0) {
  const m = antrian.notifikasi.shift()
  console.log("SMS: " + m.resi + " transit di " + m.kota)
}

Dua pesan yang tadi hilang sekarang terkirim, terlambat tapi lengkap. Broker sungguhan menyimpannya di disk: antrian RabbitMQ, Redis Streams, Amazon SQS (menyimpan pesan 4 hari kalau gak diatur, paling lama 14 hari), atau Kafka (7 hari secara bawaan). Harganya: ada penyimpanan yang harus dirawat, dan pesan bisa datang terlambat.

Kapan gak perlu pub/sub

⚠️ Jebakan yang sering ketemu

TL;DR: Pub/Sub mengirim pesan lewat broker: publisher memanggil publish(topic, message), subscriber mendaftar dengan subscribe(topic, fn), dan keduanya cuma kenal broker dan nama topik, bukan satu sama lain. Itu bedanya dengan Observer, yang subject-nya memegang daftar penerimanya sendiri. Broker menyimpan listeners[topic], membuat daftar dengan ??=, membaca dengan ?? [], dan publish mengembalikan jumlah penerima. Penerima baru gak mengubah kode publisher. Pub/sub murni gak menyimpan pesan: subscribe sesudah pesan kedua berarti cuma 1 SMS dari 3, dan topik salah ketik berarti 0. Kalau pesan gak boleh hilang, pakai antrian. Kalau butuh jawaban, panggil langsung.

Yang akan kamu pelajari