Loa ding
FLASH SALE!!! Tools Kalkulator Gym, hanya 5k dengan kode ADS5K.
Selengkapnya
Pemrograman

Transactional Outbox Pattern di Laravel untuk Atasi Dual-Write di SaaS

A 4 menit baca 2 0
Transactional Outbox Pattern di Laravel untuk Atasi Dual-Write di SaaS
Amplop tersegel menumpuk di baki outbox meja kayu di bawah lampu hangat

Order sudah ter-commit dan baris invoice tersimpan di Postgres, tapi publish ke queue gagal, timeout, atau tidak pernah jalan karena proses mati di antara dua panggilan itu. Akibatnya billing mengira order ada, sementara worker gudang tidak pernah tahu. Celah ini disebut dual-write problem, dan transactional outbox pattern adalah cara yang dibahas di dev.to untuk menutupnya di aplikasi Laravel.

Penulisnya menjalankan Loop Origin, yang membangun dan memperkuat sistem Laravel multi-tenant untuk tim produk. Ia memakai pola ini setiap kali sebuah penulisan data harus memicu efek samping yang bergantung pada service, queue, atau webhook consumer lain. Masalahnya muncul di hampir semua aplikasi yang alurnya 'simpan dulu, baru dispatch'.

Cara kerja transactional outbox pattern

Idenya sederhana. Tulis baris data bisnis dan baris outbox dalam satu transaksi database yang sama. Worker terpisah lalu membaca baris outbox yang belum terkirim, mempublikasikannya ke broker, dan menandainya sebagai published. Karena pengiriman bersifat at-least-once, bukan exactly-once, consumer wajib idempotent.

Kode naif yang menyimpan order di dalam transaksi lalu mempublikasikan pesan ke RabbitMQ di luar transaksi terlihat aman saat dites di lokal. Di production, penulis menyebut empat skenario gagal yang cukup untuk membuatnya sulit tidur:

  • Proses mati setelah commit, sebelum publish.
  • Publish berhasil, tapi proses mati sebelum keberhasilan dicatat, sehingga retry mengirim pesan dua kali.
  • Broker menerima pesan, lalu consumer crash di tengah handler.
  • Clock skew atau job yang tertunda membuat pengecekan 'sudah diproses' memberi hasil keliru.

Commit database dan publish ke broker tidak bisa dibuat atomik lintas dua sistem tanpa protokol transaksi bersama, yang tidak dijalankan kebanyakan tim. Outbox menerima kenyataan itu dan menyimpan efek samping ke storage yang tahan lama lebih dulu.

Tabel outbox dan worker publisher

Tabel outbox ditaruh di database yang sama dengan data bisnis. Untuk SaaS multi-tenant, penulis menambahkan kolom tenant_id agar tim ops bisa mengkarantina satu tenant tanpa memindai seluruh tabel. Kolom lainnya mencakup aggregate_type, aggregate_id, event_type, payload berformat jsonb, available_at, published_at, attempts, dan last_error. Kolom dedupe_key adalah kontrak dengan producer, misalnya order.created:{order_id}, dan unique index pada tenant_id plus dedupe_key mencegah insert ganda saat request di-retry dengan event logis yang sama.

Iklan

Jika transaksi rollback, order dan baris outbox sama-sama tidak ada. Jika commit, efek samping tetap aman meski Redis atau RabbitMQ mati sepuluh menit berikutnya.

Worker memakai FOR UPDATE SKIP LOCKED supaya beberapa replika bisa berjalan tanpa saling berebut baris. Dalam contohnya, worker mengambil batch 50 baris yang belum published, availablenya sudah lewat, dan attempts-nya di bawah 25, lalu menaikkan attempts. Kalau publish gagal, error disimpan dan available_at digeser dengan exponential backoff maksimal 300 detik. Dua aturan ini lebih penting daripada SDK broker yang dipakai:

  • Header publish harus membawa message_id (primary key outbox) dan dedupe_key.
  • Tandai published hanya setelah broker mengirim acknowledgment. Kalau menandai lebih dulu lalu proses crash, pesan hilang selamanya.

Jadwalkan worker rapat, yaitu setiap beberapa detik di production, atau jalankan sebagai daemon dengan sleep pendek. Penulis tetap menyimpan entri Laravel scheduler sebagai watchdog kalau daemon-nya mati.

Consumer idempotent: at-least-once adalah kontraknya

Menurut penulis, exactly-once end-to-end lewat broker hanyalah istilah pemasaran kecuali kamu punya transactional consumer dan broker yang ikut berpartisipasi. Yang realistis adalah dua hal: publish at-least-once dari outbox, dan efek samping bisnis yang effectively-once lewat handler idempotent.

Di sisi consumer, handler membuka transaksi, mengecek tabel ProcessedMessage berdasarkan dedupe_key dengan lockForUpdate, dan berhenti kalau pesannya sudah ada. Kalau belum, handler membuat WarehouseJob dari payload, lalu mencatat dedupe_key, message_id, dan waktu proses ke tabel yang sama, semuanya dalam satu transaksi.

Untuk kamu yang membangun SaaS dengan Laravel, pola ini relevan kalau ada alur seperti order yang harus sampai ke gudang, billing, atau webhook pelanggan. Kolom tenant_id di tabel outbox juga memudahkan penanganan masalah per tenant tanpa mengganggu pelanggan lain.

Iklan
Kategori Pemrograman
Bagikan
A

Ditulis oleh

Andreas Budi Ekayana

Andreas Budi Ekayana adalah pendiri AndreDEV Studio, studio pengembangan digital di Bandung yang berdiri sejak 2011. Berbekal pengalaman lebih dari satu dekade sebagai developer dan desainer, Andreas menulis seputar tekn...

Lihat artikel lainnya

Komentar 0

Belum ada komentar. Jadilah yang pertama berbagi pendapat.

Tulis komentar

Komentar dimoderasi otomatis. Mohon gunakan bahasa yang sopan. 0 / 1500