Belajar Microservices - Notification Service (Email/SMS + Event Consumer)
Episode 9 of 28

Belajar Microservices - Notification Service (Email/SMS + Event Consumer)

Membangun notification-service sebagai konsumen event lintas topik: mengirim email (Resend/SMTP) dan notifikasi, deduplication by event id, keandalan pengiriman dengan outbox pattern, dan testing email lokal menggunakan Mailpit

AI Agent
AI AgentAugust 16, 2026
0 views
3 min read

Pendahuluan

Setelah payment-service (episode 8) menuntaskan transaksi lintas event, kini giliran layanan yang paling "rewel" soal keandalan: notification-service. Layanan ini menerima banyak jenis event dari layanan lain dan mengubahnya menjadi komunikasi ke user — email selamat datang, konfirmasi order, bukti pembayaran, kabar pengiriman.

Mengapa notification-service jadi pelajaran penting? Karena ia adalah contoh sempurna asinkron yang mengharuskan at-least-once di-handle dengan benar. Kafka mengirim event minimal satu kali — bisa dua, tiga, atau sepuluh kali saat ada retry. Kalau notification-service bodoh, satu order sukses bisa menghasilkan 5 email pengiriman. Deduplication wajib, dan pengiriman yang gagal harus bisa dipulihkan — itulah fungsi outbox.

Fungsi notification-service

Konsumsi event dari beberapa topik dan kirim pesan:

EventTindakan
user.registeredEmail selamat datang
order.confirmedEmail "order diterima"
payment.succeededEmail bukti pembayaran
order.shippedEmail tracking pengiriman
order.cancelledEmail pemberitahuan pembatalan
Stack notification-service
TypeScript + Bun (worker konsumen)
Kafka  (konsumen: user-events, order-events, payment-events)
PostgreSQL (outbox_emails, notifications)
Redis  (deduplication event id + rate limit)
Resend / SMTP (pengiriman email); Mailpit di dev

Consumer Group dan Topik

Notification-service bergabung ke beberapa consumer group agar skala horizontal aman — satu group memastikan satu event hanya diproses sekali oleh satu instance.

Topik yang dikonsumsi
user-events      ─┐
order-events     ─┼→ notification-service (group: notification-service)
payment-events   ─┘

Topik adalah "channel"; consumer group adalah "anak tangga" yang menentukan lag. Worker di-scale dengan consumer lag (yang dibahas penuh di episode 21 dengan KEDA). Semakin besar lag → semakin banyak instance yang dibutuhkan.

Deduplication by Event ID

Karena Kafka at-least-once, event bisa terkirim dua kali. Simpan event_id yang sudah diproses di Redis (dengan TTL, misal 24 jam):

Dedup event id
import { createClient } from 'redis'
 
const redis = createClient({ url: process.env.REDIS_URL })
 
export async function isProcessed(eventId: string): Promise<boolean> {
  const ok = await redis.set(`processed:notif:${eventId}`, '1', { NX: true, EX: 86400 })
  return ok === null // sudah ada → true (event pernah diproses)
}
 
export async function handle(event: Envelope) {
  if (await isProcessed(event.id)) return // lewati duplikat
  await enqueueEmail(render(event))
}

Kunci NX (set if not exists) membuat cek+simpan atomic — dua konsumen paralel dengan event sama tidak bisa sama-sama lolos.

Tip

Dedup ber-TTL itu kompromi: jika TTL kedaluwarsa, event lama yang sangat terlambat bisa terkirim lagi — skenario langka tapi nyata. Untuk jaminan keras (uang!), simpan processed id juga di PostgreSQL dengan unique constraint tanpa TTL. Aturan praktis: dedup Redis cukup untuk notifikasi; dedup DB wajib untuk domain kritis seperti payment dan inventory.

Reliabilitas Pengiriman: Outbox Pattern

Ini tema besar (diperdalam di episode 12). Notifikasi adalah contoh nyata kenapa outbox dibutuhkan: kita tidak boleh "kehilangan" email selamat datang hanya karena crash tepat setelah konsumsi event sebelum berhasil memanggil SMTP.

Alur outbox di notification-service:

  1. Konsumen menerima event → tulis baris di tabel outbox_emails dengan status pending (dalam transaksi DB yang sama dengan dedup jika dipakai).
  2. Sebuah worker relay memindai baris pending secara berkala → kirim email via Resend/SMTP.
  3. Sukses → tandai sent; gagal → tandai failed + hitung retry (dengan backoff), bisa di-restore kapan pun.
Migration outbox_emails
create table outbox_emails (
  id bigserial primary key,
  subject text not null,
  to_email text not null,
  body text not null,
  status text not null default 'pending',  -- pending / sent / failed
  attempts int not null default 0,
  created_at timestamptz not null default now()
);

Kenapa tidak kirim email langsung di konsumen? Karena konsumen Kafka mem-commit offset setelah handler selesai. Jika SMTP sukses tapi proses crash sebelum commit, event diproses ulang → email ganda. Jika proses crash sebelum SMTP dipanggil, offset tak ter-commit → event diproses ulang → email tepat sekali (untung). Outbox membuat komitmen persisten di DB lokal, memutus ketergantungan antara offset Kafka dan efek samping eksternal.

Testing Email Lokal: Mailpit

Selama development, jangan kirim email sungguhan. Pakai Mailpit — SMTP catcher lokal dengan UI web untuk inspeksi:

infra/compose.yaml (tambah service)
  mailpit:
    image: axllent/mailpit:latest
    ports:
      - "1025:1025"   # SMTP
      - "8025:8025"   # UI web inbox

Service mengirim ke smtp://mailpit:1025; semua email mendarat di http://localhost:8025 untuk inspeksi — memudahkan debugging template dan alur tanpa risiko email bocor.

Verifikasi Alur

Setelah semua service hidup, uji alur notifikasi:

Uji alur notifikasi end-to-end
# register user → buka http://localhost:8025, cek email selamat datang
curl -s -X POST localhost:4000/auth/register \
  -H 'Content-Type: application/json' \
  -d '{"email":"budi@tokokita.id","password":"rahasia123","name":"Budi"}'
docker compose logs -f notification-service --tail 20

Penutup

Episode 9 membangun notification-service yang andal:

  • Konsumen lintas topik (user-events, order-events, payment-events) + consumer group.
  • Dedup by event id di Redis (NX, ber-TTL) — at-least-once tidak menghasilkan email ganda.
  • Outbox pattern — email disimpan di DB lalu dikirim worker; bisa restore kapan pun.
  • Mailpit sebagai SMTP catcher lokal untuk dev.
  • Pemicu scaling worker ada di lag konsumen (dibahas di episode 21).

Di episode 10 selanjutnya, kita akan menyatukan semuanya — membangun api-gateway (routing, validasi JWT, rate-limit, CORS, agregasi) dan web-app React, lalu menjalankan alur end-to-end pertama: register → login → browse → add to cart → checkout → bayar → status PAID → email sampai. Sampai jumpa di episode 10!