DEBI PRAHARADIKA
← Back to Blog Index
Backend Architecture2026-08-2412 min read

Membangun Resilient Message Publishing dengan Transactional Outbox Pattern di MySQL

Bagaimana cara memastikan sinkronisasi antara database utama dan Message Broker tanpa kehilangan data? Pelajari Transactional Outbox Pattern di MySQL.

"Failures are a given and everything will eventually fail over time. Architecture is about how to prevent those failures from bleeding into your business logic." — Werner Vogels, CTO Amazon

Dalam merancang sistem terdistribusi skala besar, terutama arsitektur Microservices, kita sering kali bertemu dengan dilema yang dikenal dengan The Dual-Write Problem.

Bayangkan sebuah skenario pembuatan pesanan (order) di aplikasi e-commerce. Secara logis, sistem Anda harus melakukan dua tindakan krusial secara bersamaan:

  1. Menyimpan data transaksi pesanan ke database MySQL.
  2. Mengirimkan pesan (event) "OrderCreated" ke Message Broker (seperti RabbitMQ atau Apache Kafka) agar layanan lain (contoh layanan Logistik dan Pembayaran) bisa merespons pesanan tersebut.

Mengapa Pendekatan Sinkron Tidak Ideal?

Masalah akan muncul jika Anda mengeksekusi kedua langkah di atas secara sinkron / berurutan tanpa menggunakan pengamanan:

  • Skenario Gagal 1: Data berhasil disimpan ke MySQL, tetapi tiba-tiba koneksi jaringan ke RabbitMQ terputus. Pesan gagal terkirim. Akibatnya, uang pelanggan sudah terpotong di database, namun layanan logistik tidak pernah mengirimkan barangnya.
  • Skenario Gagal 2: Pesan berhasil dikirim ke RabbitMQ, tetapi ketika sistem mencoba melakukan commit ke MySQL, terjadi Deadlock. Akibatnya, kurir logistik datang menjemput barang, padahal data pesanannya fiktif / tidak pernah tercatat di database.

Ketidaksinkronan data ini merupakan risiko fatal bagi integritas sistem. Untuk menyelesaikan masalah ini tanpa harus mengorbankan performa akibat kerumitan Two-Phase Commit (2PC), salah satu pola arsitektur yang direkomendasikan adalah Transactional Outbox Pattern.

Transactional Outbox Pattern

Ide dasarnya sebenarnya cukup sederhana. Daripada memaksakan aplikasi untuk berkomunikasi dengan dua sistem yang berbeda (MySQL dan RabbitMQ) di saat bersamaan, kita dapat mengalihkannya fokusnya murni ke database relasional. Daripada langsung mengirim event ke pihak eksternal, aplikasi cukup menyimpan payload pesan tersebut ke dalam sebuah tabel khusus (misal tabel outbox) yang berada di database yang sama dengan tabel bisnis utama.

Arsitektur Transactional Outbox Pattern Data Model Architecture: Transaksi lokal menjamin bahwa penyimpanan ke 'Business Table' (misal: tabel orders) dan 'Outbox Table' dieksekusi secara atomik. Jika salah satu gagal, keduanya otomatis dibatalkan (rollback).

Pendekatan ini sepenuhnya menumpang pada fitur ACID Transactions (Atomicity, Consistency, Isolation, Durability) bawaan MySQL. Praktiknya berjalan seperti ini: ketika pengguna membuat pesanan, aplikasi di tingkat backend akan membuka sebuah blok transaksi lokal (BEGIN). Di dalam blok pelindung ini, aplikasi pertama-tama akan menyimpan data pesanan ke tabel orders. Namun, sebelum transaksi tersebut ditutup, aplikasi juga meracik payload pesan berformat JSON dan langsung menyisipkannya ke tabel outbox_messages.

Setelah kedua instruksi penyimpanan tersebut selesai, barulah aplikasi mengeksekusi perintah COMMIT. Berkat sifat Atomicity dari database, MySQL memberikan jaminan bahwa kedua tabel tersebut pasti berhasil ditulis secara permanen, atau keduanya dibatalkan (rollback) jika terjadi insiden sekecil apa pun. Tidak akan pernah ada status setengah jadi; tidak ada lagi skenario di mana data pesanan tersimpan namun event gagal terkirim ke Message Broker.

Dengan cara ini, kita telah meniadakan kemungkinan pesan hilang. Lalu, bagaimana pesan dari tabel outbox_messages bisa sampai ke RabbitMQ?

Komponen The Message Relay

Di sinilah komponen sekunder bekerja. Kita harus membangun sebuah layanan terpisah yang bertugas memindahkan pesan dari tabel outbox ke Message Broker. Terdapat dua metodologi utama untuk membangun Message Relay:

  1. Polling Publisher - Sederhana namun Berat: Sebuah cron job atau worker daemon yang secara konstan melakukan SELECT * FROM outbox_messages WHERE status = 'pending', mengirimkannya ke RabbitMQ, lalu meng-update statusnya menjadi 'published'. Pendekatan ini mudah dibuat namun akan membebani database jika dilakukan terlalu sering.
  2. Change Data Capture / CDC - Kompleks namun Instan: Menggunakan perangkat lunak seperti Debezium, sistem akan membaca langsung aliran log biner (MySQL Binlog). Begitu ada baris baru masuk ke tabel outbox, CDC akan menangkapnya lalu memublikasikannya ke Kafka/RabbitMQ. Pendekatan ini menjadi best practice terbaik.

Implementasi / Deployment

Polling Publisher

Mari kita implementasikan Transactional Outbox Pattern menggunakan bahasa Golang dan MySQL. Kita akan menggunakan kueri SQL agar batas transaksinya terlihat jelas. Berikut adalah contoh implementasi Polling Publisher menggunakan Golang.

package main

import (
	"context"
	"database/sql"
	"encoding/json"
	"log"

	_ "github.com/go-sql-driver/mysql"
)

// Struktur payload pesan yang akan dikirim ke RabbitMQ/Kafka
type OrderEvent struct {
	OrderID   string  `json:"order_id"`
	Total     float64 `json:"total"`
	EventType string  `json:"event_type"`
}

func CreateOrderWithOutbox(db *sql.DB, orderID string, total float64) error {
	// 1. Memulai Transaksi database (ACID Boundary)
	tx, err := db.BeginTx(context.Background(), nil)
	if err != nil {
		return err
	}
	
	// Memastikan Rollback dieksekusi jika terjadi panic atau error
	defer tx.Rollback()

	// 2. Menyisipkan data utama ke Business Table (tabel orders)
	_, err = tx.ExecContext(context.Background(), 
		"INSERT INTO orders (id, total, status) VALUES (?, ?, 'CREATED')", 
		orderID, total)
	if err != nil {
		return err // Otomatis ter-rollback berkat defer
	}

	// 3. Merangkai payload pesan JSON
	event := OrderEvent{
		OrderID:   orderID,
		Total:     total,
		EventType: "ORDER_CREATED_EVENT",
	}
	payload, _ := json.Marshal(event)

	// 4. Menyisipkan pesan ke Outbox Table di dalam transaksi yang sama
	_, err = tx.ExecContext(context.Background(),
		"INSERT INTO outbox_messages (aggregate_type, aggregate_id, payload, status) VALUES (?, ?, ?, 'PENDING')",
		"Order", orderID, string(payload))
	if err != nil {
		return err // Otomatis ter-rollback
	}

	// 5. Atomic Commit (Keduanya berhasil atau gagal bersamaan)
	if err = tx.Commit(); err != nil {
		return err
	}

	log.Printf("Order %s dan Outbox Event berhasil dikomit secara atomik!", orderID)
	return nil
}

Eksekusi

Pada implementasi di atas, perhatikan bahwa tidak ada sama sekali pemanggilan API ke RabbitMQ di dalam fungsi CreateOrderWithOutbox(). Kita membebaskan enduser dari harus menunggu latensi jaringan dari sistem eksternal. Proses mempublikasikan pesan akan dikerjakan secara asinkron (latar belakang) oleh Message Relay.

Contoh Kasus dan Desain UI

Kami telah mengimplementasikan arsitektur ini pada sistem Pemesanan Tiket Pesawat (Online Travel Agent (OTA)) klien Travel Agent kami. Sistem mereka dulunya sering kehilangan event webhook saat mentransfer dana karena ketidakstabilan koneksi ke Kafka. Sistem ini juga memanfaatkan MySQL sebagai primary database dan CDC tool untuk memonitor perubahan data pada tabel outbox. Dengan beralih ke Transactional Outbox Pattern dan menggunakan Debezium sebagai CDC Relay, mereka mencapai angka Zero Data Loss.

Dashboard Outbox Message Relay UI System Design: Dasbor pemantauan Outbox Message Relay. Anda dapat memonitor aliran 'Pending Messages' yang belum diproses oleh CDC, serta metrik 'Relay Latency' yang menunjukkan jeda antara penyimpanan database dan keberhasilan publikasi (hanya memakan waktu belasan milidetik).

Pada dasbor di atas, arsitek software dapat memantau antrean. Jika RabbitMQ mati selama 2 jam, data tidak akan hilang. Pesan-pesan tersebut hanya akan berstatus PENDING di dalam tabel MySQL. Begitu RabbitMQ menyala kembali, Message Relay akan menjalankan semua pesanan yang tertunda secara berurutan.


Transactional Outbox Pattern bukanlah sekadar teknik opsional, melainkan fondasi bagi sistem terdistribusi yang menuntut tingkat ketahanan / resiliency. Dengan memadukan logika bisnis dan logika pengiriman pesan ke dalam satu ikatan transaksional ACID, kita menghilangkan akar masalah ketidakkonsistenan data. Meskipun membutuhkan sedikit effort dalam perancangan tabel outbox dan pemeliharaan Message Relay, harga tersebut setara dengan ketenangan pikiran karena zero data loss.

Mari tetap semangat belajar, bereksplorasi, dan jangan pernah berhenti membangun hal-hal yang luar biasa.