Trigger vs pekerja latar belakang untuk notifikasi yang andal
Pelajari kapan trigger atau pekerja tugas latar belakang lebih aman untuk notifikasi, dengan panduan praktis tentang pengulangan percobaan, batasan transaksi, dan pencegahan duplikasi.

Mengapa pengiriman notifikasi sering gagal di aplikasi nyata
Notifikasi terdengar sederhana: pengguna melakukan sesuatu, lalu email atau SMS dikirim. Sebagian besar kegagalan nyata disebabkan oleh timing dan duplikasi. Pesan terkirim sebelum data benar-benar tersimpan, atau terkirim dua kali setelah kegagalan parsial.
“Notifikasi” bisa berarti banyak hal: email struk, kode sekali pakai lewat SMS, push alert, pesan dalam aplikasi, ping Slack atau Telegram, atau webhook ke sistem lain. Masalah yang sama selalu muncul: Anda mencoba mengoordinasikan perubahan database dengan sesuatu di luar aplikasi.
Dunia luar berantakan. Provider bisa lambat, mengembalikan timeout, atau menerima permintaan sementara aplikasi Anda tidak menerima respons sukses. Aplikasi Anda sendiri bisa crash atau restart di tengah permintaan. Bahkan pengiriman yang “sukses” bisa dijalankan ulang karena retry infrastruktur, restart worker, atau pengguna menekan tombol lagi.
Penyebab umum kegagalan pengiriman notifikasi termasuk timeout jaringan, gangguan atau pembatasan rate provider, restart aplikasi pada momen yang salah, retry yang menjalankan ulang logika pengiriman tanpa penjaga unik, dan desain di mana penulisan database dan pengiriman eksternal terjadi sebagai satu langkah gabungan.
Saat orang meminta “notifikasi yang andal,” mereka biasanya berarti salah satu dari dua hal:
- dikirim tepat sekali, atau
- paling tidak tidak menggandakan (duplikat seringkali lebih buruk daripada keterlambatan).
Mendapatkan keduanya—cepat dan aman sempurna—sulit, sehingga Anda harus memilih kompromi antara kecepatan, keamanan, dan kompleksitas.
Itu sebabnya pilihan antara trigger dan worker latar belakang bukan hanya debat arsitektur. Ini tentang kapan pengiriman diizinkan terjadi, bagaimana kegagalan di-retry, dan bagaimana Anda mencegah email atau SMS ganda saat sesuatu salah.
Trigger dan worker latar belakang: apa maksudnya
Ketika orang membandingkan trigger dengan worker latar belakang, sebenarnya mereka membandingkan di mana logika notifikasi berjalan dan seberapa erat ia terikat dengan aksi yang menyebabkannya.
Trigger berarti “lakukan sekarang ketika X terjadi.” Dalam banyak aplikasi, itu berarti mengirim email atau SMS segera setelah aksi pengguna, di dalam request web yang sama. Trigger juga bisa berada di tingkat database: trigger database berjalan otomatis saat baris disisipkan atau diperbarui. Kedua tipe terasa langsung, tetapi mereka mewarisi timing dan batasan dari apa pun yang memicunya.
Worker latar belakang berarti “lakukan segera, tapi bukan di foreground.” Ini adalah proses terpisah yang menarik job dari queue dan berusaha menyelesaikannya. Aplikasi utama Anda mencatat apa yang harus terjadi, lalu segera merespons, sementara worker menangani bagian yang lebih lambat dan rentan gagal seperti memanggil provider email atau SMS.
“Job” adalah unit kerja yang diproses worker. Biasanya mencakup siapa yang akan diberi tahu, template mana, data apa yang harus diisi, status saat ini (queued, processing, sent, failed), berapa banyak upaya yang telah dilakukan, dan kadang waktu terjadwal.
Alur notifikasi tipikal terlihat seperti ini: Anda menyiapkan detail pesan, memasukkan job ke antrian, mengirim lewat provider, mencatat hasil, lalu memutuskan apakah akan retry, berhenti, atau memberi tahu seseorang.
Batasan transaksi: kapan aman benar-benar aman untuk mengirim
Batasan transaksi adalah garis antara “kami mencoba menyimpan ini” dan “ini benar-benar tersimpan.” Sampai database commit, perubahan masih bisa di-rollback. Itu penting karena notifikasi sulit ditarik kembali.
Jika Anda mengirim email atau SMS sebelum commit, Anda bisa memberi tahu seseorang tentang sesuatu yang sebenarnya tidak terjadi. Seorang pelanggan bisa menerima “Kata sandi Anda telah diubah” atau “Pesanan Anda terkonfirmasi,” lalu penulisan gagal karena error constraint atau timeout. Sekarang pengguna bingung, dan dukungan harus meluruskannya.
Mengirim dari dalam trigger database terlihat menggoda karena langsung terpanggil saat data berubah. Namun masalahnya trigger berjalan di dalam transaksi yang sama. Jika transaksi di-rollback, Anda mungkin sudah memanggil provider email atau SMS.
Trigger database juga cenderung lebih sulit untuk diamati, diuji, dan di-retry dengan aman. Ketika mereka melakukan panggilan eksternal yang lambat, mereka bisa menahan lock lebih lama dari yang diharapkan dan membuat masalah database lebih sulit didiagnosis.
Pendekatan yang lebih aman adalah ide outbox: rekam niat untuk memberi tahu sebagai data, commit, lalu kirim sesudahnya.
Anda melakukan perubahan bisnis dan, dalam transaksi yang sama, menyisipkan baris outbox yang menjelaskan pesan (siapa, apa, channel, plus kunci unik). Setelah commit, worker latar belakang membaca baris outbox yang tertunda, mengirim pesan, lalu menandainya sebagai terkirim.
Pengiriman langsung masih bisa oke untuk pesan berdampak rendah dan bersifat informasional dimana salah dikirim dapat diterima, seperti “Kami memproses permintaan Anda.” Untuk apa pun yang harus cocok dengan status akhir, tunggu sampai setelah commit.
Retry dan penanganan kegagalan: di mana masing-masing pendekatan unggul
Retry biasanya menjadi faktor penentu.
Trigger: cepat, tapi rapuh saat gagal
Sebagian besar desain berbasis trigger tidak punya cerita retry yang baik.
Jika trigger memanggil provider email/SMS dan panggilan gagal, Anda biasanya berakhir dengan dua pilihan buruk:
- gagal transaksi (dan memblokir update asli), atau
- menangkap error dan mengabaikannya (dan kehilangan notifikasi secara diam-diam).
Keduanya tidak dapat diterima ketika keandalan penting.
Mencoba melakukan loop atau delay di dalam trigger bisa memperburuk kondisi dengan membuat transaksi terbuka lebih lama, meningkatkan waktu lock, dan memperlambat database. Dan jika database atau app mati di tengah pengiriman, seringkali Anda tidak bisa tahu apakah provider menerima permintaan.
Worker latar belakang: dirancang untuk retry
Worker memperlakukan pengiriman sebagai tugas terpisah dengan state-nya sendiri. Itu membuatnya wajar untuk melakukan retry hanya bila masuk akal.
Secara praktis, Anda biasanya me-retry kegagalan sementara (timeout, isu jaringan sementara, error server, rate limit dengan jeda lebih lama). Anda biasanya tidak me-retry masalah permanen (nomor telepon tidak valid, email salah format, penolakan keras seperti pengguna unsubscribe). Untuk error “tidak diketahui”, batasi jumlah upaya dan buat state terlihat.
Backoff menjaga retry agar tidak memperburuk masalah. Mulai dengan jeda singkat, lalu tingkatkan setiap kali (misalnya 10s, 30s, 2m, 10m), dan berhenti setelah jumlah percobaan tertentu.
Agar ini bertahan saat deploy dan restart, simpan state retry dengan setiap job: jumlah percobaan, waktu percobaan berikutnya, error terakhir (singkat dan mudah dibaca), waktu percobaan terakhir, dan status yang jelas seperti pending, sending, sent, failed.
Jika app Anda restart saat pengiriman, worker dapat memeriksa job yang macet (misalnya status = sending dengan timestamp lama) dan me-retry dengan aman. Di sinilah idempoten menjadi penting agar retry tidak menggandakan pengiriman.
Mencegah duplikasi email dan SMS dengan idempoten
Idempoten berarti Anda bisa menjalankan aksi “kirim notifikasi” lebih dari sekali dan pengguna tetap menerima pesan satu kali.
Kasus duplikasi klasik adalah timeout: aplikasi memanggil provider email atau SMS, request timeout, dan kode Anda retry. Permintaan pertama mungkin sebenarnya berhasil, jadi retry membuat duplikat.
Perbaikan praktis adalah memberi setiap pesan kunci stabil dan memperlakukan kunci itu sebagai sumber kebenaran tunggal. Kunci yang baik menjelaskan apa arti pesan, bukan kapan Anda mencoba mengirimnya.
Pendekatan umum termasuk:
notification_idyang dibuat saat Anda memutuskan “pesan ini harus ada,” atau- kunci turunan dari bisnis seperti
order_id + template + recipient(hanya jika itu benar-benar mendefinisikan keunikan).
Kemudian simpan ledger pengiriman (seringkali tabel outbox itu sendiri) dan biarkan semua retry memeriksanya sebelum mengirim. Pertahankan state yang sederhana dan terlihat: created (diputuskan), queued (siap), sent (terkonfirmasi), failed (gagal terkonfirmasi), canceled (tidak lagi diperlukan). Aturan krusial adalah membolehkan hanya satu record aktif per kunci idempoten.
Idempoten di sisi provider bisa membantu bila didukung, tetapi itu tidak menggantikan ledger Anda sendiri. Anda masih perlu menangani retry, deploy, dan restart worker.
Perlakukan juga hasil “tidak diketahui” sebagai kelas pertama. Jika request timeout, jangan segera mengirim lagi. Tandai sebagai pending konfirmasi dan retry dengan aman dengan memeriksa status pengiriman provider bila memungkinkan. Jika Anda tidak bisa konfirmasi, tunda dan beri tahu alih-alih mengirim ganda.
Pola default yang aman: outbox + worker latar belakang (langkah demi langkah)
Jika Anda ingin default yang aman, pola outbox ditambah worker sulit dikalahkan. Ini menjauhkan pengiriman dari transaksi bisnis Anda, sambil tetap menjamin niat untuk memberi tahu tersimpan.
Alur
Anggap “kirim notifikasi” sebagai data yang Anda simpan, bukan aksi yang Anda tembakkan.
Anda menyimpan perubahan bisnis (misalnya, status pesanan). Dalam transaksi database yang sama, Anda juga menyisipkan record outbox dengan penerima, channel (email/SMS), template, payload, dan kunci idempoten. Commit transaksi. Hanya setelah titik itu sesuatu boleh dikirim.
Worker latar belakang secara berkala mengambil baris outbox yang tertunda, mengirimkannya, dan mencatat hasil.
Tambahkan langkah klaim sederhana agar dua worker tidak mengambil baris yang sama. Ini bisa berupa perubahan status ke processing atau timestamp terkunci.
Mencegah duplikat dan menangani kegagalan
Duplikat sering terjadi ketika pengiriman berhasil tetapi aplikasi Anda crash sebelum mencatat “sent.” Anda atasi itu dengan membuat penulisan “mark sent” aman untuk diulang.
Gunakan aturan unik (misalnya constraint unik pada kunci idempoten dan channel). Retry dengan aturan jelas: percobaan terbatas, delay meningkat, dan hanya untuk error yang bisa di-retry. Setelah percobaan terakhir, pindahkan job ke status dead-letter (mis. failed_permanent) agar seseorang dapat meninjau dan memproses ulang secara manual.
Monitoring bisa sederhana: jumlah pending, processing, sent, retrying, dan failed_permanent, plus timestamp pending tertua.
Contoh konkret: ketika pesanan berubah dari “Packed” ke “Shipped,” Anda mengupdate baris order dan membuat satu baris outbox dengan kunci idempoten order-4815-shipped. Bahkan jika worker crash saat mengirim, rerun tidak akan menggandakan karena penulisan “sent” dilindungi oleh kunci unik itu.
Kapan worker latar belakang lebih cocok
Trigger database bagus untuk merespons saat data berubah. Tetapi jika tugasnya adalah “mengirim notifikasi dengan andal di kondisi dunia nyata yang berantakan,” worker latar belakang biasanya memberi Anda kontrol lebih.
Worker lebih cocok bila Anda membutuhkan pengiriman berbasis waktu (reminder, digest), volume tinggi dengan rate limit dan backpressure, toleransi terhadap variabilitas provider (batas 429, respons lambat, outage singkat), workflow multi-langkah (kirim, tunggu pengiriman, lalu tindak lanjut), atau event lintas-sistem yang perlu rekonsiliasi.
Contoh sederhana: Anda mencharge pelanggan, lalu mengirim struk SMS, lalu email faktur. Jika SMS gagal karena gateway, Anda tetap ingin pesanan tetap berstatus dibayar dan ingin retry aman nanti. Meletakkan logika itu di trigger berisiko mencampurkan “data benar” dengan “pihak ketiga tersedia sekarang.”
Worker juga mempermudah kontrol operasional. Anda bisa menjeda antrian saat insiden, memeriksa kegagalan, dan retry dengan jeda.
Kesalahan umum yang menyebabkan pesan hilang atau duplikat
Cara tercepat untuk mendapatkan notifikasi yang tidak dapat diandalkan adalah “tinggal kirim dimana nyaman,” lalu berharap retry menyelamatkan. Apakah Anda menggunakan trigger atau worker, detail di sekitar kegagalan dan state yang menentukan apakah pengguna mendapat satu pesan, dua pesan, atau tidak sama sekali.
Perangkap umum adalah mengirim dari trigger database dan berasumsi itu tidak bisa gagal. Trigger berjalan di dalam transaksi database, jadi panggilan provider yang lambat bisa menahan write, terkena timeout, atau mengunci tabel lebih lama dari perkiraan. Lebih buruk, jika pengiriman gagal dan Anda rollback transaksi, Anda mungkin nanti retry dan mengirim dua kali jika provider sebenarnya menerima panggilan pertama.
Kesalahan yang sering muncul berulang:
- Melakukan retry untuk semua hal dengan cara yang sama, termasuk error permanen (email buruk, nomor diblokir).
- Tidak memisahkan "queued" dari "sent", sehingga Anda tidak tahu apa yang aman untuk di-retry setelah crash.
- Menggunakan timestamp sebagai kunci dedupe, sehingga retry mudah melewati aturan unik.
- Melakukan panggilan provider di jalur request pengguna (checkout dan submit form seharusnya tidak menunggu gateway).
- Memperlakukan timeout provider sebagai “tidak terkirim”, padahal banyak yang sebenarnya “tidak diketahui.”
Contoh sederhana: Anda mengirim SMS, provider timeout, dan Anda retry. Jika request pertama sebenarnya berhasil, pengguna menerima dua kode. Perbaikannya adalah mencatat kunci idempoten yang stabil (mis. notification_id), menandai pesan sebagai queued sebelum mengirim, lalu menandainya sebagai sent hanya setelah respons sukses yang jelas.
Pemeriksaan cepat sebelum merilis notifikasi
Sebagian besar bug notifikasi bukan soal alat. Mereka soal timing, retry, dan catatan yang hilang.
Pastikan Anda hanya mengirim setelah penulisan database benar-benar di-commit. Jika Anda mengirim di dalam transaksi yang kemudian di-rollback, pengguna bisa mendapat pesan tentang sesuatu yang tidak pernah terjadi.
Selanjutnya, beri setiap notifikasi identitas unik. Berikan setiap pesan kunci idempoten stabil (mis. order_id + event_type + channel) dan tegakkan di penyimpanan sehingga retry tidak bisa membuat notifikasi “baru” kedua.
Sebelum rilis, cek hal-hal dasar ini:
- Pengiriman terjadi setelah commit, bukan selama penulisan.
- Setiap notifikasi punya kunci idempoten unik, dan duplikat ditolak.
- Retry aman: sistem bisa menjalankan job yang sama lagi dan tetap mengirim paling banyak sekali.
- Setiap percobaan dicatat (status, last_error, timestamps).
- Percobaan dibatasi, dan item macet punya tempat jelas untuk ditinjau dan diproses ulang.
Uji perilaku restart dengan sengaja. Bunuh worker saat mengirim, restart, dan verifikasi tidak ada pengiriman ganda. Lakukan hal yang sama saat database sedang under load.
Skenario sederhana untuk divalidasi: pengguna mengganti nomor telepon, lalu Anda mengirim SMS verifikasi. Jika provider timeout, aplikasi Anda retry. Dengan kunci idempoten dan log percobaan yang baik, Anda akan mengirim sekali atau mencoba lagi nanti dengan aman—tetapi tidak spam.
Contoh skenario: pembaruan pesanan tanpa ganda-kirim
Sebuah toko mengirim dua jenis pesan: (1) email konfirmasi pesanan setelah pembayaran, dan (2) SMS update saat paket sedang dikirim dan saat diterima.
Berikut yang salah ketika Anda mengirim terlalu cepat (mis. di dalam trigger database): langkah pembayaran menulis baris orders, trigger terpanggil dan mengirim email, lalu capture pembayaran gagal beberapa detik kemudian. Sekarang ada email “Terima kasih atas pesanan Anda” untuk pesanan yang tidak jadi nyata.
Sekarang bayangkan masalah sebaliknya: status pengiriman berubah menjadi “Out for delivery”, Anda memanggil provider SMS, dan provider timeout. Anda tidak tahu apakah pesan terkirim. Jika Anda langsung retry, risiko dua SMS. Jika tidak retry, risiko tidak ada SMS sama sekali.
Alur yang lebih aman menggunakan record outbox ditambah worker latar belakang. Aplikasi commit order atau perubahan status, dan dalam transaksi yang sama menulis baris outbox seperti “kirim template X ke user Y, channel SMS, kunci idempoten Z.” Hanya setelah commit worker akan mengirim pesan.
Timeline sederhana seperti ini:
- Pembayaran sukses, transaksi commit, baris outbox untuk email konfirmasi tersimpan.
- Worker mengirim email, lalu menandai outbox sebagai sent dengan message ID dari provider.
- Status pengiriman berubah, transaksi commit, baris outbox untuk update SMS tersimpan.
- Provider timeout, worker menandai outbox sebagai retryable dan mencoba lagi nanti dengan kunci idempoten yang sama.
Saat retry, baris outbox adalah sumber kebenaran tunggal. Anda tidak membuat permintaan “kirim” kedua, Anda menyelesaikan yang pertama.
Untuk dukungan, ini juga lebih jelas. Mereka bisa melihat pesan yang macet di failed dengan last_error (timeout, nomor buruk, email diblokir), berapa banyak percobaan, dan apakah aman untuk retry tanpa menggandakan.
Langkah berikutnya: pilih pola dan implementasikan dengan rapi
Pilih default dan dokumentasikan. Perilaku tidak konsisten biasanya muncul dari mencampur trigger dan worker secara acak.
Mulai kecil dengan tabel outbox dan satu loop worker. Tujuan pertama bukan kecepatan, tapi kebenaran: simpan apa yang Anda niatkan untuk dikirim, kirim setelah commit, dan tandai sebagai sent hanya ketika provider mengonfirmasi.
Rencana rollout sederhana:
- Definisikan event (order_paid, ticket_assigned) dan channel yang bisa digunakan.
- Tambahkan tabel outbox dengan event_id, recipient, payload, status, attempts, next_retry_at, sent_at.
- Bangun satu worker yang polling baris pending, mengirim, dan memperbarui status di satu tempat.
- Tambahkan idempoten dengan kunci unik per pesan dan “jangan lakukan apa-apa jika sudah terkirim.”
- Bagi error menjadi retryable (timeout, 5xx) vs tidak retryable (nomor buruk, email diblokir).
Sebelum Anda skala volume, tambahkan visibilitas dasar. Lacak jumlah pending, rate kegagalan, dan umur pesan pending tertua. Jika pending tertua terus bertambah, besar kemungkinan worker macet, provider outage, atau bug logika.
Jika Anda membangun di AppMaster (appmaster.io), pola ini cocok: modelkan outbox di Data Designer, tulis perubahan bisnis dan baris outbox dalam satu transaksi, lalu jalankan logika send-and-retry di proses latar belakang terpisah. Pemisahan itu yang menjaga pengiriman notifikasi tetap andal bahkan saat provider atau deploy bermasalah.
FAQ
Background worker biasanya merupakan default yang lebih aman karena pengiriman itu lambat dan rentan gagal; worker dirancang untuk retry dan visibilitas. Trigger bisa cepat, tetapi mereka terkait erat dengan transaksi atau request yang memicunya, sehingga kegagalan dan duplikasi lebih sulit ditangani dengan bersih.
Berbahaya karena penulisan ke database masih bisa dibatalkan (roll back). Anda bisa saja memberi tahu pengguna tentang pesanan, perubahan kata sandi, atau pembayaran yang pada akhirnya tidak ter-commit, dan Anda tidak bisa “membatalkan” email atau SMS setelah dikirim.
Trigger database berjalan di dalam transaksi yang sama dengan perubahan baris. Jika trigger memanggil provider email/SMS dan transaksi kemudian gagal, Anda mungkin sudah mengirim pesan nyata tentang perubahan yang tidak tersimpan, atau transaksi bisa terblokir karena pemanggilan eksternal yang lambat.
Pola outbox menyimpan niat untuk mengirim sebagai baris di database Anda, dalam transaksi yang sama dengan perubahan bisnis. Setelah commit, worker membaca baris outbox yang tertunda, mengirim pesan, lalu menandainya sebagai tersentuh, yang membuat timing dan retry jauh lebih aman.
Jika provider timeout, hasil sebenarnya sering kali “tidak diketahui”, bukan “gagal”. Sistem yang baik mencatat percobaan, menunda, dan melakukan retry dengan aman menggunakan identitas pesan yang sama, alih-alih langsung mengirim lagi dan berisiko duplikasi.
Gunakan idempoten: berikan setiap notifikasi kunci stabil yang merepresentasikan makna pesan (bukan kapan Anda mencoba mengirim). Simpan kunci itu di ledger (seringkali tabel outbox) dan terapkan satu record aktif per kunci, sehingga retry menyelesaikan pesan yang sama alih-alih membuat yang baru.
Retry kesalahan sementara seperti timeout, respons 5xx, atau rate limit (dengan jeda). Jangan retry kesalahan permanen seperti alamat tidak valid, nomor diblokir, atau hard bounces; tandai sebagai gagal dan buat terlihat supaya data bisa diperbaiki alih-alih terus mengirim ulang.
Worker latar belakang dapat memindai job yang macet di status sending lebih lama dari batas waktu yang wajar, memindahkannya ke retryable, dan mencoba lagi dengan backoff. Ini aman hanya jika setiap job memiliki state yang tercatat (percobaan, timestamp, last_error) dan idempoten mencegah pengiriman ganda.
Artinya Anda bisa menjawab “aman untuk retry?” Simpan status jelas seperti pending, processing, sent, dan failed, plus hitungan percobaan dan last_error. Itu membuat dukungan dan debugging praktis, serta memungkinkan sistem pulih tanpa menebak.
Modelkan tabel outbox di Data Designer, tulis update bisnis dan baris outbox dalam satu transaksi, lalu jalankan logic send-and-retry di proses latar belakang terpisah. Jaga satu kunci idempotensi per pesan dan catat percobaan, sehingga deploy, retry, dan restart worker tidak membuat duplikat.


