DEBI PRAHARADIKA
← Back to Blog Index
System Design2026-07-0212 min read

Membangun Sistem Notifikasi Asynchronous Menggunakan Message Broker

Panduan teknis mengatasi bottleneck API dengan arsitektur asynchronous menggunakan Go, Redis, dan React.

Bayangkan pengguna Anda baru saja mengklik tombol "Export Laporan Penjualan Q3". Laporan tersebut sangat besar dan membutuhkan waktu 15 detik untuk di-generate oleh server. Jika Anda menggunakan pendekatan konvensional (sinkron / synchronous), browser pengguna akan loading memutar terus menerus selama 15 detik penuh, membuat mereka tidak bisa melakukan apa-apa, dan berisiko mengalami Timeout Error. Ini adalah mimpi buruk dari User Experience (UX).

Solusi untuk masalah ini adalah Asynchronous Processing. Daripada menahan pengguna menunggu, API akan langsung membalas: "Permintaan sedang diproses." di detik pertama, lalu sebuah sistem di belakang layar (Background Worker) akan mengerjakan tugas tersebut. Setelah selesai, Worker akan "memberi tahu" pengguna melalui notifikasi real-time.

Untuk menjembatani komunikasi ini dengan cepat, kita menggunakan perangkat lunak khusus yang disebut Message Broker. Di artikel ini, kita akan membangun sistem notifikasi asynchronous menggunakan kombinasi bahasa Go, Redis, dan antarmuka React.


Daftar Isi

  1. System Thinking & Arsitektur Asynchronous
  2. Setup Message Broker: Redis
  3. Backend: Golang Producer (Main API)
  4. Backend: Golang Consumer (Background Worker)
  5. Frontend: UI React + Tailwind CSS
  6. Kesimpulan & Best Practices

1. System Thinking & Arsitektur Asynchronous

Arsitektur ini didasarkan pada pemisahan tugas (Decoupling). Main API Server yang melayani request pengguna dipisahkan dari Worker Server yang mengeksekusi tugas-tugas berat.

Arsitektur Asynchronous Notification

Alur Kerja (Workflow):

  1. Request Masuk: React App memanggil API Go untuk tugas berat (misal: /api/export-report).
  2. Publish Event: Main API (Producer) mengambil data request, membuat pesan (payload JSON), dan melemparnya ke antrean/kanal di Redis. Main API langsung membalas HTTP 202 Accepted ke React.
  3. Penyimpanan Pesan: Redis menyimpan atau mem-publish pesan tersebut secara efisien di memori.
  4. Consume Event: Background Worker yang juga ditulis dalam Go mendengarkan kanal Redis secara terus-menerus. Saat pesan masuk, Worker mulai mengeksekusi logic berat (membuat PDF laporan).
  5. Push Notification: Setelah PDF selesai dibuat, Worker menembakkan notifikasi via WebSocket kembali ke React App pengguna, yang memunculkan ikon lonceng di layar mereka.

2. Setup Message Broker: Redis

Kita akan menggunakan Redis karena sangat ringan, sangat cepat karena bekerja di memori (in-memory), dan memiliki fitur bawaan untuk Message Queuing (via Redis Lists) maupun komunikasi event-driven (via Redis Pub/Sub).

Untuk tutorial ini, kita akan menggunakan pendekatan Redis List (LPUSH dan BRPOP) agar pesan dapat diantrekan (jika worker mati, pesan tidak hilang begitu saja).

Pastikan Redis sudah berjalan di local Anda (via Docker):

docker run -d --name redis-server -p 6379:6379 redis:alpine

3. Backend: Golang Producer (Main API)

Pertama-tama, mari kita buat server API utama di Go. Server ini bertanggung jawab menerima permintaan pengguna dan "mendorong" pesan ke Redis.

Instalasi library Redis untuk Go:

go get github.com/redis/go-redis/v9

api.go

package main

import (
	"context"
	"encoding/json"
	"log"
	"net/http"

	"github.com/redis/go-redis/v9"
)

var ctx = context.Background()
var rdb *redis.Client

type ExportTask struct {
	UserID   string `json:"user_id"`
	ReportID string `json:"report_id"`
	Format   string `json:"format"`
}

func main() {
	// Koneksi ke Redis
	rdb = redis.NewClient(&redis.Options{
		Addr: "localhost:6379",
	})

	http.HandleFunc("/api/export", handleExport)

	log.Println("Main API Server running on port 8080...")
	http.ListenAndServe(":8080", nil)
}

func handleExport(w http.ResponseWriter, r *http.Request) {
	// Simulasi mengambil data dari request
	task := ExportTask{
		UserID:   "user_1029",
		ReportID: "report_sales_q3",
		Format:   "pdf",
	}

	payload, _ := json.Marshal(task)

	// Melempar pesan ke antrean Redis (List) bernama "export_queue"
	err := rdb.LPush(ctx, "export_queue", payload).Err()
	if err != nil {
		http.Error(w, "Gagal memasukkan antrean", http.StatusInternalServerError)
		return
	}

	// Langsung respon pengguna tanpa menunggu proses selesai
	w.WriteHeader(http.StatusAccepted)
	w.Write([]byte(`{"message": "Permintaan export diterima dan sedang diproses."}`))
}

4. Backend: Golang Consumer (Background Worker)

Sekarang, kita buat aplikasi terpisah yang akan berjalan terus di belakang layar sebagai Background Worker. Worker ini akan menunggu (memblokir sementara) hingga ada pesan baru di antrean Redis.

worker.go

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"time"

	"github.com/redis/go-redis/v9"
)

var ctx = context.Background()

type ExportTask struct {
	UserID   string `json:"user_id"`
	ReportID string `json:"report_id"`
	Format   string `json:"format"`
}

func main() {
	rdb := redis.NewClient(&redis.Options{
		Addr: "localhost:6379",
	})

	log.Println("Background Worker started. Menunggu tugas di 'export_queue'...")

	for {
		// BRPOP memblokir proses sampai ada data di queue. (0 berarti tidak ada timeout)
		result, err := rdb.BRPop(ctx, 0, "export_queue").Result()
		if err != nil {
			log.Println("Error:", err)
			continue
		}

		// Result[0] adalah nama queue, Result[1] adalah payload JSON
		payload := result[1]
		var task ExportTask
		json.Unmarshal([]byte(payload), &task)

		processExport(task)
	}
}

func processExport(task ExportTask) {
	log.Printf("Mulai memproses export untuk User: %s, Report: %s\n", task.UserID, task.ReportID)
	
	// Simulasi pekerjaan berat (membuat PDF)
	time.Sleep(5 * time.Second) 
	
	log.Printf("Selesai! File %s.%s siap.\n", task.ReportID, task.Format)

	// PENTING: Di titik ini, worker akan memanggil server WebSocket 
	// (misal Pusher atau Socket.io server) untuk mengirimkan event ke Frontend.
	sendWebSocketNotification(task.UserID, "Report Export Completed")
}

func sendWebSocketNotification(userID string, message string) {
	// (Implementasi pengiriman event WebSockets ke klien spesifik)
	fmt.Printf("[WebSocket PUSH] To: %s -> %s\n", userID, message)
}

5. Frontend: UI React + Tailwind CSS

Di sisi klien, setelah permintaan /api/export dikirim, UI tidak memblokir pengguna. Pengguna dapat terus bekerja. Begitu event masuk via WebSockets, sebuah notifikasi elegan akan muncul di sudut atas aplikasi.

Berikut rancangan Dropdown Notifikasi bergaya SaaS Swiss Minimalism.

Desain UI Notifikasi Asinkron

import React, { useState, useEffect } from 'react';
// import { io } from 'socket.io-client';

export default function NotificationDropdown() {
  const [isOpen, setIsOpen] = useState(false);
  const [notifications, setNotifications] = useState([
    {
      id: 1,
      title: 'Report Export Completed',
      message: "The monthly 'Sales Performance Report (Q3)' is ready for download.",
      time: 'Just now',
      unread: true,
      type: 'success'
    }
  ]);

  // Simulasi mendengarkan event WebSocket dari Go Worker
  /*
  useEffect(() => {
    const socket = io('wss://api.domain.com');
    socket.on('notification_received', (data) => {
       setNotifications(prev => [data, ...prev]);
    });
  }, []);
  */

  return (
    <div className="relative font-sans">
      {/* Ikon Lonceng */}
      <button 
        onClick={() => setIsOpen(!isOpen)}
        className="relative p-2 text-gray-800 hover:bg-gray-100 rounded-md transition"
      >
        <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
        </svg>
        {/* Unread Badge */}
        {notifications.some(n => n.unread) && (
          <span className="absolute top-1 right-1 flex h-3 w-3">
            <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75"></span>
            <span className="relative inline-flex rounded-full h-3 w-3 bg-blue-600"></span>
          </span>
        )}
      </button>

      {/* Dropdown Panel */}
      {isOpen && (
        <div className="absolute right-0 mt-2 w-80 bg-white border border-gray-200 shadow-xl z-50">
          <div className="flex justify-between items-center px-4 py-3 border-b border-gray-100">
            <h3 className="font-semibold text-gray-800 text-sm">Notifications</h3>
            <button className="text-xs text-gray-500 hover:text-black">Mark all as read</button>
          </div>
          
          <div className="max-h-96 overflow-y-auto">
            {notifications.map((notif) => (
              <div key={notif.id} className="relative flex p-4 border-b border-gray-50 hover:bg-gray-50 transition cursor-pointer">
                {/* Unread indicator dot */}
                {notif.unread && <div className="absolute left-2 top-6 w-1.5 h-1.5 rounded-full bg-blue-600"></div>}
                
                <div className="pl-3 w-full">
                  <div className="flex justify-between items-start">
                    <p className="text-sm font-medium text-gray-900">{notif.title}</p>
                    <span className="text-xs text-gray-400">{notif.time}</span>
                  </div>
                  <p className="text-sm text-gray-600 mt-1 line-clamp-2 leading-snug">{notif.message}</p>
                </div>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

6. Kesimpulan & Best Practices

Menggunakan antrean pesan asynchronous membawa aplikasi Anda menuju skalabilitas sejati. Ketika permintaan membludak (misalnya saat promosi akhir bulan), Main API tidak akan pernah crash karena ia hanya mengoper data tersebut ke Redis. Background Worker akan mengambil data dari Redis secara bertahap sesuai kapasitas sistem.

Untuk level Production Enterprise, perhatikan beberapa best practices ini:

  1. Dead Letter Queue (DLQ): Jika Worker gagal memproses tugas (misalnya karena file source hilang), pindahkan pesan yang gagal tersebut ke antrean khusus (DLQ) agar bisa dicek kembali oleh teknisi (jangan dihapus otomatis).
  2. Idempotency: Terkadang message broker bisa mengirim satu pesan dua kali karena anomali jaringan. Pastikan logic di dalam fungsi Go processExport Anda bersifat idempotent (jika dipanggil dua kali dengan ReportID yang sama, tidak akan menyebabkan data berantakan atau error fatal).
  3. Pilih Skala Sesuai Kebutuhan: Jika antrean Anda membutuhkan fitur pertukaran pesan yang sangat kompleks dengan banyak cabang dan jaminan (guarantee delivery), RabbitMQ adalah pilihan utama. Namun jika sekadar antrean memori yang cepat dan lebih sederhana, Redis adalah pilihannya.

Dengan arsitektur ini, Anda bukan sekadar menulis program, tapi sedang membangun sebuah orkestra perangkat lunak (software orchestration). Selamat mencoba!