Sebuah service sering perlu mengubah state database sekaligus menerbitkan event dalam satu operasi. Sebuah order dapat berpindah ke status paid sementara OrderPaid harus sampai ke message broker. Kedua penulisan itu melewati sistem berbeda, sehingga transaksi database biasa tidak dapat membuat kedua commit menjadi atomik.
Menulis ke database lebih dahulu menyisakan celah: proses dapat berhenti setelah commit tetapi sebelum event diterbitkan. Menerbitkan event lebih dahulu menciptakan celah sebaliknya: consumer dapat menerima event untuk perubahan database yang kemudian gagal.
Pola transactional outbox menghapus keputusan yang terpisah itu. Service menulis perubahan bisnis dan record outbox yang durable dalam transaksi database lokal yang sama. Relay terpisah kemudian menerbitkan record outbox yang sudah commit ke broker.
Satu transaksi mencatat state dan niat penerbitan
Outbox row mewakili kewajiban untuk menerbitkan event, bukan bukti bahwa penerbitan sudah terjadi.
Transaksi sederhana dapat berbentuk seperti ini:
BEGIN;
UPDATE orders
SET status = 'paid'
WHERE id = 'ord_123';
INSERT INTO outbox_events (
event_id,
aggregate_id,
event_type,
payload,
created_at
) VALUES (
'evt_456',
'ord_123',
'OrderPaid',
'{"order_id":"ord_123"}',
CURRENT_TIMESTAMP
);
COMMIT;Jika transaksi rollback, kedua record tidak menjadi terlihat. Jika transaksi commit, keduanya menjadi durable bersama. Aplikasi tidak lagi memiliki crash window antara business commit dan pembuatan niat penerbitan.
Jaminan ini bergantung pada kedua row yang berada dalam transactional resource yang sama. Menempatkan outbox di database lain mengembalikan masalah distributed commit yang hendak dihindari pola ini.
Relay menangani delivery setelah commit
Setelah transaksi commit, relay mencari outbox row yang belum diterbitkan dan mengirimkannya ke broker. Relay dapat berupa polling worker, proses berbasis change data capture, atau mekanisme lain yang mengamati row yang sudah commit tanpa bergabung dengan transaksi request awal.
Polling relay dapat memakai alur seperti berikut:
repeat:
rows = claim_next_batch()
for row in rows:
publish(row.event_id, row.event_type, row.payload)
mark_published(row.event_id)Request path hanya perlu memastikan transaksi lokal berhasil. Latency broker atau gangguan broker sementara tidak harus memperpanjang transaksi database. Row yang tertunda tetap durable sampai relay dapat berjalan kembali.
Relay sebaiknya memakai batch terbatas dan strategi claim yang sesuai untuk worker konkuren. Menahan transaksi database tetap terbuka selama network publish yang lambat dapat memperpanjang lock dan mengikat kesehatan database pada latency broker, sehingga implementasi biasanya memisahkan claim dari external I/O dengan recovery semantics yang tegas.
Penerbitan biasanya bersifat at least once
Relay secara umum tidak dapat membuat publish() dan mark_published() atomik di antara broker dan database.
Perhatikan urutan berikut:
1. relay menerbitkan evt_456
2. broker menerima evt_456
3. relay berhenti sebelum menandai row sebagai published
4. relay berjalan kembali
5. evt_456 diterbitkan lagiOutbox menutup celah event yang hilang, tetapi penerbitan duplikat tetap dapat terjadi. Menganggap pola ini sebagai exactly-once delivery akan menyamarkan batas tersebut.
Karena itu, setiap event memerlukan identifier yang stabil. Consumer yang tidak dapat mengulang side effect dengan aman dapat mencatat event ID yang sudah diproses, menerapkan unique constraint pada inbox table, atau memakai mekanisme idempotency yang sesuai dengan operasinya.
Fitur broker dapat mengurangi duplikat di dalam boundary broker sendiri, tetapi tidak otomatis membuat mutasi database milik consumer menjadi atomik dengan message acknowledgement.
Ordering memerlukan scope yang eksplisit
Urutan event global tunggal jarang tersedia tanpa biaya. Banyak relay worker, partition, retry, dan routing broker dapat memengaruhi urutan yang terlihat oleh consumer.
Banyak sistem hanya memerlukan ordering per aggregate. Event untuk satu order, account, atau document dapat membawa sequence number yang meningkat monoton dan dibuat bersama perubahan state. Consumer kemudian dapat menolak versi usang, menahan gap bila sesuai, atau merutekan aggregate key yang sama ke satu broker partition yang terurut.
Timestamp saja merupakan kontrak ordering yang lemah. Presisi clock, transaksi konkuren, dan perbedaan clock dapat menghasilkan nilai yang sama atau urutan yang menyesatkan. Database sequence atau aggregate version biasanya menyatakan relasi yang dimaksud secara lebih langsung.
Jika tidak ada jaminan ordering yang diperlukan, dokumentasi yang tegas mencegah consumer bergantung pada perilaku relay yang kebetulan muncul.
Desain payload menentukan coupling
Outbox dapat menyimpan event payload lengkap atau field yang cukup bagi relay untuk membentuknya. Menyimpan payload final di dalam transaksi bisnis membekukan representasi event pada titik yang sama dengan perubahan state. Deployment berikutnya tidak dapat secara diam-diam menerbitkan row lama memakai serialization logic baru.
Stabilitas tersebut memiliki biaya: payload memakai ruang database dan schema evolution memerlukan penanganan yang disengaja. Metadata event umumnya mencakup event ID, event type, schema version, aggregate ID, creation time, serta trace atau correlation identifier bila elemen tersebut menjadi bagian dari kontrak service.
Membaca business row terkini di kemudian waktu untuk membentuk kembali event lama dapat menghasilkan fakta yang berbeda dari kondisi saat event dibuat. Jika nilai historis yang tepat penting, simpan nilai tersebut bersama outbox record.
Data sensitif juga memerlukan peninjauan retensi yang sama seperti field database durable lainnya. Menyalin secret atau data pribadi yang tidak diperlukan ke outbox memperluas lokasi yang harus dilindungi dan dihapus sesuai kebijakan.
Cleanup tidak boleh berpacu dengan delivery
Outbox table terus tumbuh kecuali row yang sudah diterbitkan diarsipkan atau dihapus. Cleanup memerlukan batas state yang tidak dapat menghapus pekerjaan yang masih memenuhi syarat untuk dikirim.
Desain praktis dapat mempertahankan published row selama interval tertentu, memindahkannya ke storage yang lebih murah, atau menghapusnya dalam batch kecil. Masa retensi dapat mendukung investigasi operasional dan prosedur replay, tetapi replay sebaiknya menjadi operasi eksplisit, bukan konsekuensi tidak sengaja dari reset sebuah flag.
Index sebaiknya mengikuti query relay. Index pada publication state ditambah kolom ordering atau creation dapat menjaga pemilihan batch tetap efisien, sedangkan terlalu banyak index menambah biaya pada setiap transaksi bisnis yang memasukkan outbox row.
Backlog besar memerlukan perhatian tersendiri. Relay yang pulih dari gangguan sebaiknya tidak menghabiskan seluruh database I/O saat mengejar backlog. Batch size, polling interval, jumlah worker, dan throughput broker membentuk satu jalur kapasitas.
Observability mengikuti durable boundary
Signal yang paling berguna menggambarkan jarak antara niat yang sudah commit dan penerbitan yang selesai. Operator dapat memantau jumlah pending row, usia pending row tertua, publish attempt, publish failure, deteksi duplikat, throughput relay, dan progres cleanup.
Usia pending row tertua sangat berguna karena antrean kecil tetap dapat menyimpan satu event yang macet dalam waktu lama. Queue depth saja dapat menyembunyikan kondisi tersebut.
Tracing dapat membawa correlation identifier dari request ke outbox row lalu ke broker header. Relay berjalan kemudian, sehingga context eksplisit ini mempertahankan hubungan antara transaksi awal dan asynchronous delivery.
Pola ini mempersempit atomicity boundary
Transactional outbox tidak mengubah database dan broker menjadi satu transaksi. Pola ini membagi masalah menjadi dua tahap dengan durable handoff di antaranya.
Tahap pertama melakukan commit secara atomik terhadap business state beserta niat penerbitan. Tahap kedua mengirimkan niat tersebut dengan retry dan penanganan duplikat. Pembagian ini memberi state yang dapat dipulihkan pada setiap kegagalan: sebelum commit tidak ada yang perlu diterbitkan; setelah commit tersedia row durable yang dapat terus diproses relay.
Boundary tersebut menjadi nilai utama pola ini. Sistem menerima penerbitan at-least-once dan idempotency pada consumer sebagai pertukaran untuk menghapus interval rapuh ketika business state sudah commit tetapi event terkait dapat hilang secara permanen.