Outbox: Ketika Tabel Database Menggantikan Message Queue & Redis
Banyak aplikasi akhirnya harus melakukan dua hal sekaligus: menyimpan data dan memberi tahu sistem lain soal itu. Pasien daftar → simpan kunjungan dan kirim laporan ke API kesehatan. Pembayaran lunas → catat transaksi dan dorong event ke sistem lain. Cara naif-nya disebut "dual write": tulis ke database, lalu kirim ke message queue. Nah, celah di antara dua langkah itulah tempat data diam-diam hilang.
Pola outbox menutup celah tersebut — dan untuk banyak sistem, artinya kamu tak butuh RabbitMQ, Kafka, atau Redis sama sekali.
Masalah "dual write"
simpanKunjungan(kunjungan) // sudah commit ke Postgres
kirimKeQueue(event) // ...jaringan ngadat di sini
Kalau proses crash di antara dua baris itu, atau queue sempat detik tak terjangkau, kamu dapat kunjungan tanpa event. Dibalik urutannya? Bisa dapat event untuk kunjungan yang tak pernah tersimpan. Tak ada transaksi yang membentang dari "database saya" ke "broker milik orang lain" — jadi selalu ada yang bisa lolos lewat celah itu.
Outbox itu sebenarnya apa
Bayangkan resepsionis klinik yang harus kirim laporan ke SATUSEHAT tiap pasien daftar. SATUSEHAT kadang down. Kalau nunggu jawaban SATUSEHAT dulu baru layani pasien, antre panjang.
Solusi outbox: resepsionis tulis "PR kirim" di buku catatan yang sama dengan buku transaksi, lalu langsung lanjut layani pasien. Ada petugas terpisah yang tiap menit buka buku itu, kerjakan PR yang belum beres, coret yang sudah terkirim. Teknisnya: satu baris tabel di database yang sama, ditulis dalam satu transaksi bersama data bisnis.
CREATE TABLE outbox_event (
id text PRIMARY KEY DEFAULT gen_random_uuid()::text,
type text NOT NULL, -- 'encounter', 'payment', ...
payload jsonb NOT NULL,
status text NOT NULL DEFAULT 'pending', -- pending | sent
attempts int NOT NULL DEFAULT 0,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX idx_outbox_pending ON outbox_event (created_at) WHERE status = 'pending';
Jalur tulis — satu transaksi, tak ada yang bisa desinkron:
BEGIN;
INSERT INTO kunjungan (...) VALUES (...);
INSERT INTO outbox_event (type, payload) VALUES ('encounter', $1);
COMMIT;
Dan worker yang men-drain-nya, pakai timer:
for (const e of await listPending()) {
try {
await kirimKeApiLuar(e.payload);
await markSent(e.id);
} catch (err) {
await markFailed(e.id, err); // tetap pending, dicoba lagi ronde berikut
}
}
Segitu saja. To-do list yang tinggal di buku yang sama dengan data asli, plus petugas yang menyicilnya.
Kenapa ini bisa gantikan message queue dan Redis
Untuk tugas ini, broker sebenarnya cuma memberi satu layanan penting: menyimpan pesan yang belum terproses, tanpa hilang. Tabel database bisa lakukan itu juga.
- Durability (tak hilang saat restart). Baris tersimpan di disk Postgres. Restart container, redeploy, crash — baris pending tetap ada, sama seperti Redis-persistence atau MQ durable.
- Konsistensi — di sinilah outbox justru menang. Baris bisnis dan baris "kirim ini" commit bareng, atomik. Tak ada jendela di mana salah satu ada tanpa yang lain. Dengan broker terpisah, kamu balik ke dual-write: commit DB bisa sukses sementara publish gagal (atau sebaliknya), dan sistemmu jadi tak sinkron.
- Satu komponen lebih sedikit. Tak perlu server tambahan untuk di-install, diamankan, dimonitor, dan di-backup. Database yang sudah kamu jalankan mengurus data dan antrean.
Kapan broker sungguhan tetap lebih baik
| Kebutuhan | Tabel outbox | MQ / Redis |
|---|---|---|
| Durable, anti-hilang | Ya | Ya |
| Atomik dengan datamu | Ya (keunggulannya) | Tidak — dual write |
| Volume rendah–sedang | Ya | Ya |
| Throughput sangat tinggi (ribuan/detik) | Tidak — polling membebani DB | Ya |
| Latency real-time (milidetik, push) | Tidak — nunggu ronde poll | Ya |
| Backoff canggih, dead-letter, fan-out banyak consumer | Bikin manual | Bawaan |
Kesimpulan
Kalau volume-mu sedang saja dan yang benar-benar penting adalah "jangan sampai event hilang atau dobel", pola outbox memberimu durability sekaligus konsistensi transaksi tanpa infrastruktur tambahan. Baru pakai Kafka/RabbitMQ/Redis saat kamu melampauinya — throughput tinggi, latency real-time, atau retry berjenjang — bukan sebelum itu.
Analoginya: outbox = to-do list di buku yang sama dengan buku transaksi. Message queue = kantor pos terpisah — lebih canggih, tapi kamu jadi harus menjaga dua buku tetap sinkron, dan justru di situlah bug biasa muncul.
$ cat comments/ (0)