12 ago 2025·7 min di lettura

Trigger vs processi in background per notifiche affidabili

Scopri quando usare trigger o worker in background per notifiche più sicure, con indicazioni pratiche su retry, transazioni e prevenzione dei duplicati.

Trigger vs processi in background per notifiche affidabili

Perché la consegna delle notifiche si rompe nelle app reali

Le notifiche sembrano semplici: un utente fa qualcosa, poi parte un'email o un SMS. La maggior parte dei guasti reali dipendono da tempistiche e duplicazioni. I messaggi vengono inviati prima che i dati siano veramente salvati, oppure vengono inviati due volte dopo un fallimento parziale.

Una “notifica” può essere molte cose: ricevute via email, codici one-time via SMS, avvisi push, messaggi in-app, ping su Slack o Telegram, o un webhook verso un altro sistema. Il problema comune è sempre lo stesso: stai cercando di coordinare una modifica del database con qualcosa al di fuori della tua app.

Il mondo esterno è disordinato. I provider possono essere lenti, restituire timeout o accettare una richiesta mentre la tua app non riceve la risposta di successo. La tua app può andare in crash o riavviarsi a metà richiesta. Anche gli invii “riusciti” possono essere rieseguiti a causa di retry dell’infrastruttura, riavvii dei worker o perché un utente ha premuto il pulsante di nuovo.

Cause comuni di consegna delle notifiche interrotta includono timeout di rete, outage o limiti di rate dei provider, riavvii dell’app nel momento sbagliato, retry che rieseguono la stessa logica di invio senza una guardia unica, e progettazioni dove una scrittura al database e un invio esterno avvengono come un unico passo combinato.

Quando le persone chiedono “notifiche affidabili”, di solito intendono una di due cose:

  • consegnare esattamente una volta, oppure
  • almeno non duplicare (i duplicati spesso sono peggiori di un ritardo).

Ottenere entrambe le cose, veloci e perfettamente sicure, è difficile, quindi si finisce per scegliere compromessi tra velocità, sicurezza e complessità.

Per questo la scelta tra trigger e worker in background non è solo un dibattito architetturale. Riguarda quando è permesso inviare, come vengono gestiti i retry e come si prevengono email o SMS duplicati quando qualcosa va storto.

Trigger e worker in background: cosa significano

Quando si confrontano trigger e worker in background, in realtà si confronta dove gira la logica di notifica e quanto è legata all’azione che l’ha causata.

Un trigger è “fallo ora quando X succede”. In molte app questo significa inviare un’email o un SMS subito dopo l’azione dell’utente, dentro la stessa richiesta web. I trigger possono anche vivere a livello di database: un trigger di database esegue automaticamente quando una riga viene inserita o aggiornata. Entrambi i tipi sembrano immediati, ma ereditano i vincoli di tempo e i limiti di chi li ha attivati.

Un worker in background è “fallo presto, ma non in primo piano”. È un processo separato che prende job da una coda e prova a completarli. La tua app principale registra cosa deve succedere e risponde velocemente, mentre il worker gestisce le parti lente e soggette a errori come chiamare un provider email o SMS.

Un “job” è l’unità di lavoro che il worker elabora. Tipicamente include chi notificare, quale template, quali dati inserire, lo stato corrente (queued, processing, sent, failed), quanti tentativi sono stati fatti e a volte un orario programmato.

Un tipico flusso di notifica è: prepari i dettagli del messaggio, metti in coda un job, invii tramite un provider, registri il risultato e poi decidi se ritentare, fermarti o allertare qualcuno.

Confini di transazione: quando è davvero sicuro inviare

Un confine di transazione è la linea tra “abbiamo provato a salvarlo” e “è davvero salvato”. Fino al commit del database, la modifica può ancora essere rollbackata. Questo è importante perché le notifiche sono difficili da ritirare.

Se invii un’email o un SMS prima del commit, puoi notificare qualcuno di qualcosa che in realtà non è successo. Un cliente potrebbe ricevere “La tua password è stata cambiata” o “Il tuo ordine è confermato”, e poi la scrittura fallisce a causa di un errore di vincolo o di un timeout. Ora l’utente è confuso e il supporto deve sbrogliare la situazione.

L’invio dall’interno di un trigger del database sembra allettante perché si attiva automaticamente quando i dati cambiano. Il problema è che i trigger girano nella stessa transazione. Se la transazione viene rollbackata, potresti aver già chiamato un provider email o SMS.

I trigger del database inoltre tendono a essere più difficili da osservare, testare e ritentare in sicurezza. E quando eseguono chiamate esterne lente, possono tenere lock più a lungo del previsto e rendere i problemi del database più difficili da diagnosticare.

Un approccio più sicuro è l’idea dell’outbox: registra l’intenzione di notificare come dato, fai il commit, poi invia.

Esegui la modifica di business e, nella stessa transazione, inserisci una riga outbox che descrive il messaggio (chi, cosa, quale canale e una chiave unica). Dopo il commit, un worker di background legge le righe outbox pendenti, invia il messaggio e poi le marca come inviate.

Gli invii immediati possono comunque andar bene per messaggi a basso impatto e puramente informativi dove sbagliare è accettabile, come “Stiamo elaborando la tua richiesta”. Per tutto ciò che deve corrispondere allo stato finale, aspetta il commit.

Retry e gestione degli errori: dove ognuno eccelle

I retry sono spesso il fattore decisivo.

Trigger: veloci, ma fragili in caso di errori

La maggior parte dei design basati su trigger non ha una buona strategia di retry.

Se un trigger chiama un provider email/SMS e la chiamata fallisce, di solito restano due scelte pessime:

  • far fallire la transazione (e bloccare l’aggiornamento originale), oppure
  • ignorare l’errore (e perdere la notifica silenziosamente).

Nessuna delle due è accettabile quando l’affidabilità conta.

Cercare di loopare o ritardare all’interno di un trigger può peggiorare le cose tenendo le transazioni aperte più a lungo, aumentando i tempi di lock e rallentando il database. E se il database o l’app muoiono a metà invio, spesso non puoi sapere se il provider ha ricevuto la richiesta.

Worker in background: progettati per i retry

Un worker tratta l’invio come un task separato con uno stato proprio. Questo rende naturale ritentare solo quando ha senso.

Come regola pratica, di solito ritenti gli errori temporanei (timeout, problemi di rete transitori, errori server, limiti di rate con attesa più lunga). Di solito non ritenti i problemi permanenti (numeri di telefono non validi, email malformate, rifiuti netti come utenti disiscritti). Per errori “sconosciuti”, limiti i tentativi e rendi lo stato visibile.

Il backoff è ciò che impedisce ai retry di peggiorare la situazione. Inizia con un’attesa breve e aumentala ogni volta (ad esempio 10s, 30s, 2m, 10m), e fermati dopo un numero fisso di tentativi.

Per far sì che questo sopravviva a deploy e riavvii, memorizza lo stato dei retry con ogni job: conteggio tentativi, tempo del prossimo tentativo, ultimo errore (breve e leggibile), ultimo tentativo e uno stato chiaro come pending, sending, sent, failed.

Se la tua app si riavvia a metà invio, un worker può ricontrollare job bloccati (ad esempio status = sending con timestamp vecchio) e ritentarli in sicurezza. Qui l’idempotenza diventa essenziale affinché un retry non invii due volte.

Prevenire email e SMS duplicati con l’idempotenza

Traccia ogni tentativo
Crea un semplice registro con stati come pending, processing, sent e failed.
Inizia ora

L’idempotenza significa poter eseguire la stessa azione “invia notifica” più volte e l’utente la riceve comunque una sola volta.

Il caso classico di duplicazione è un timeout: la tua app chiama un provider email o SMS, la richiesta va in timeout e il codice ritenta. La prima richiesta potrebbe essere effettivamente riuscita, così il retry genera un duplicato.

Una soluzione pratica è dare a ogni messaggio una chiave stabile e trattare quella chiave come fonte di verità. Buone chiavi descrivono cosa significa il messaggio, non quando hai provato a inviarlo.

Approcci comuni includono:

  • un notification_id generato quando decidi “questo messaggio deve esistere”, oppure
  • una chiave derivata dal business come order_id + template + recipient (solo se definisce davvero l’unicità).

Poi conserva un registro di invio (spesso la stessa tabella outbox) e fai sì che tutti i retry lo consultino prima di inviare. Mantieni gli stati semplici e visibili: created (deciso), queued (pronto), sent (confermato), failed (fallimento confermato), canceled (non più necessario). La regola critica è permettere solo un record attivo per chiave di idempotenza.

L’idempotenza lato provider può aiutare quando è supportata, ma non sostituisce il tuo ledger. Devi comunque gestire retry, deploy e riavvii dei worker.

Tratta anche gli esiti “sconosciuti” come prima classe. Se una richiesta è andata in timeout, non inviare subito di nuovo. Marchiala come in sospeso di conferma e ritenta in modo sicuro verificando lo stato di consegna del provider quando possibile. Se non puoi confermare, ritarda e segnala invece di inviare doppio.

Un pattern sicuro di default: outbox + worker in background (passo dopo passo)

Se vuoi un default sicuro, il pattern outbox più un worker è difficile da battere. Tiene l’invio fuori dalla transazione di business, garantendo comunque che l’intento di notificare sia salvato.

Il flusso

Tratta “inviare una notifica” come un dato che memorizzi, non come un’azione che spari subito.

Salvi la modifica di business (per esempio l’aggiornamento dello stato di un ordine). Nella stessa transazione inserisci anche una riga outbox con destinatario, canale (email/SMS), template, payload e una chiave di idempotenza. Fai il commit della transazione. Solo dopo questo punto può avvenire l’invio.

Un worker in background prende regolarmente le righe outbox pendenti, le invia e registra il risultato.

Aggiungi un semplice step di claim così due worker non prendono la stessa riga. Questo può essere un cambio di stato a processing o un timestamp di lock.

Bloccare i duplicati e gestire i fallimenti

I duplicati spesso accadono quando un invio riesce ma la tua app va in crash prima di registrare “sent”. Risolvi questo rendendo l’operazione “marca come inviato” ripetibile.

Usa una regola di unicità (per esempio un vincolo unico sulla chiave di idempotenza e sul canale). Ritenta con regole chiare: tentativi limitati, ritardi crescenti e solo per errori retryable. Dopo l’ultimo retry, sposta il job in uno stato di dead-letter (come failed_permanent) così qualcuno può esaminarlo e riprocessarlo manualmente.

Il monitoraggio può rimanere semplice: conteggi di pending, processing, sent, retrying e failed_permanent, più il timestamp della riga pending più vecchia.

Esempio concreto: quando un ordine passa da “Packed” a “Shipped”, aggiorni la riga dell’ordine e crei una riga outbox con chiave di idempotenza order-4815-shipped. Anche se il worker va in crash a metà invio, i rerun non duplicheranno perché la scrittura “sent” è protetta da quella chiave unica.

Quando i worker in background sono la scelta migliore

Usa oggi il pattern outbox
Costruisci un flusso outbox + worker in modo che email e SMS vengano inviati solo dopo il commit.
Prova AppMaster

I trigger del database sono bravi a reagire nel momento in cui i dati cambiano. Ma se il lavoro è “consegnare una notifica in modo affidabile nelle condizioni reali”, i worker in genere ti danno più controllo.

I worker sono più adatti quando hai invii basati sul tempo (promemoria, digest), volumi elevati con limiti di rate e backpressure, tolleranza alla variabilità del provider (limiti 429, risposte lente, outage temporanei), workflow multi-step (invia, aspetta la consegna, poi segui) o eventi cross-system che richiedono riconciliazione.

Esempio semplice: addebiti un cliente, poi invii un ricevuta SMS e infine un’email con la fattura. Se l’SMS fallisce per un problema con il gateway, vuoi comunque che l’ordine resti pagato e vuoi un retry sicuro dopo. Mettere quella logica in un trigger rischia di mescolare “i dati sono corretti” con “un terzo è disponibile ora”.

I worker rendono anche più facile il controllo operativo. Puoi mettere in pausa una coda durante un incidente, ispezionare i fallimenti e ritentare con ritardi.

Errori comuni che causano messaggi mancati o duplicati

Distribuisci il worker di invio
Distribuisci il tuo worker su AppMaster Cloud o sul tuo cloud quando sei pronto.
Inizia a costruire

Il modo più veloce per ottenere notifiche inaffidabili è “inviare dove è comodo” e sperare che i retry risolvano. Che tu usi trigger o worker, i dettagli intorno a errori e stato decidono se gli utenti ricevono un messaggio, due o nessuno.

Una trappola comune è inviare da un trigger di database e assumere che non possa fallire. I trigger girano nella transazione, quindi qualsiasi chiamata lenta al provider può bloccare la scrittura, causare timeout o tenere lock più a lungo del previsto. Peggio, se l’invio fallisce e rollbacki la transazione, potresti ritentare dopo e inviare due volte se il provider aveva effettivamente accettato la prima chiamata.

Errori che si ripetono spesso:

  • Ritentare tutto nello stesso modo, inclusi errori permanenti (email sbagliata, numero bloccato).
  • Non separare “queued” da “sent”, così non sai cosa è sicuro ritentare dopo un crash.
  • Usare timestamp come chiavi di deduplica, così i retry aggirano naturalmente l’unicità.
  • Fare chiamate ai provider nel percorso della richiesta utente (checkout o submit non dovrebbero aspettare i gateway).
  • Trattare i timeout dei provider come “non consegnato”, quando molti esiti sono in realtà “sconosciuti”.

Esempio semplice: invii un SMS, il provider va in timeout e tu ritenti. Se la prima richiesta è effettivamente riuscita, l’utente riceve due codici. La soluzione è registrare una chiave di idempotenza stabile (come notification_id), marcare il messaggio come queued prima dell’invio e poi come sent solo dopo una chiara risposta di successo.

Controlli rapidi prima di lanciare le notifiche

La maggior parte dei bug nelle notifiche non riguarda lo strumento ma tempistiche, retry e record mancanti.

Conferma che invii solo dopo che la scrittura sul database è stata definitivamente committata. Se invii dentro la stessa transazione e questa viene rollbackata, gli utenti possono ricevere una notifica su qualcosa che non è mai accaduto.

Poi, rendi ogni notifica identificabile in modo univoco. Assegna a ogni messaggio una chiave di idempotenza stabile (per esempio order_id + event_type + channel) e falla rispettare nello storage in modo che un retry non possa creare una seconda notifica “nuova”.

Prima del rilascio, controlla questi punti base:

  • L’invio avviene dopo il commit, non durante la scrittura.
  • Ogni notifica ha una chiave di idempotenza unica e i duplicati sono rifiutati.
  • I retry sono sicuri: il sistema può eseguire lo stesso job di nuovo e inviare al massimo una volta.
  • Ogni tentativo è registrato (stato, last_error, timestamp).
  • I tentativi sono limitati e gli elementi bloccati hanno un posto chiaro per essere rivisti e riprocessati.

Testa il comportamento al riavvio intenzionalmente. Uccidi il worker a metà invio, riavvialo e verifica che non vengano inviati duplicati. Ripeti lo stesso test con il database sotto carico.

Scenario semplice per convalidare: un utente cambia numero di telefono e invii un SMS di verifica. Se il provider SMS va in timeout e la tua app ritenta, con una buona chiave di idempotenza e un registro dei tentativi mandi al massimo una volta o ritenti in modo sicuro senza spam.

Scenario d’esempio: aggiornamenti d’ordine senza doppio invio

Implementa il default sicuro
Prototipa rapidamente il pattern completo: modello dati, logica di business e UI per il monitoraggio.
Prova AppMaster

Un negozio invia due tipi di messaggi: (1) una email di conferma ordine subito dopo il pagamento, e (2) SMS di aggiornamento quando il pacco è in consegna e consegnato.

Ecco cosa va storto se invii troppo presto (per esempio dentro un trigger del database): il passo di pagamento scrive una riga in orders, il trigger si attiva ed emaila il cliente, e poi la cattura del pagamento fallisce un secondo dopo. Ora hai una email “Grazie per il tuo ordine” per un ordine che non è mai diventato reale.

Ora immagina il problema opposto: lo stato di consegna cambia in “Out for delivery”, chiami il provider SMS e il provider va in timeout. Non sai se ha inviato il messaggio. Se ritenti subito, rischi due SMS. Se non ritenti, rischi di non inviarne nessuno.

Un flusso più sicuro usa una riga outbox e un worker in background. L’app committa l’ordine o il cambio di stato, e nella stessa transazione scrive una riga outbox tipo “invia template X a utente Y, canale SMS, chiave di idempotenza Z”. Solo dopo il commit un worker consegna i messaggi.

Un semplice timeline è:

  • Il pagamento riesce, la transazione committa e la riga outbox per l’email di conferma è salvata.
  • Il worker invia l’email e marca l’outbox come sent con un ID messaggio del provider.
  • Lo stato di consegna cambia, la transazione committa e la riga outbox per l’aggiornamento SMS è salvata.
  • Il provider va in timeout, il worker marca l’outbox come retryable e ci riprova più tardi usando la stessa chiave di idempotenza.

Al retry, la riga outbox è la singola fonte di verità: non stai creando una seconda richiesta di invio, stai completando la prima.

Per il supporto è anche più chiaro. Possono vedere messaggi bloccati in failed con l’ultimo errore (timeout, numero sbagliato, email bloccata), quanti tentativi sono stati fatti e se è sicuro ritentare senza doppio invio.

Prossimi passi: scegli un pattern e implementalo con cura

Scegli un default e documentalo. Un comportamento incoerente spesso viene da una mescolanza casuale di trigger e worker.

Inizia in piccolo con una tabella outbox e un ciclo di worker. L’obiettivo iniziale non è la velocità ma la correttezza: memorizza ciò che intendi inviare, invialo dopo il commit e marca come inviato solo quando il provider conferma.

Un semplice piano di rollout:

  • Definisci gli eventi (order_paid, ticket_assigned) e i canali che possono usare.
  • Aggiungi una tabella outbox con event_id, recipient, payload, status, attempts, next_retry_at, sent_at.
  • Costruisci un worker che interroga le righe pendenti, invia e aggiorna lo stato in un unico posto.
  • Aggiungi idempotenza con una chiave unica per messaggio e “non fare nulla se già inviato”.
  • Dividi gli errori in retryable (timeout, 5xx) vs non retryable (numero sbagliato, email bloccata).

Prima di scalare il volume, aggiungi visibilità di base. Monitora il conteggio dei pendenti, il tasso di fallimento e l’età del messaggio pendente più vecchio. Se l’elemento pendente più vecchio continua a crescere, probabilmente hai un worker bloccato, un outage del provider o un bug di logica.

Se stai costruendo in AppMaster (appmaster.io), questo pattern si mappa chiaramente: modella l’outbox nel Data Designer, scrivi l’aggiornamento di business e la riga outbox in una transazione, poi esegui la logica di invio-e-retry in un processo background separato. Questa separazione è ciò che mantiene affidabile la consegna delle notifiche anche quando provider o deploy si comportano male.

FAQ

Should I use triggers or background workers for notifications?

I worker in background sono solitamente la scelta più sicura perché l’invio è lento e soggetto a errori, e i worker sono pensati per gestire retry e visibilità. I trigger possono essere rapidi, ma sono strettamente legati alla transazione o alla richiesta che li ha attivati, il che rende più difficile gestire fallimenti e duplicati in modo pulito.

Why is it risky to send a notification before the database commit?

È rischioso perché la scrittura nel database può ancora essere annullata. Potresti finire per notificare gli utenti di un ordine, una modifica password o un pagamento che in realtà non è stato confermato, e non puoi “annullare” un’email o un SMS dopo che è stato inviato.

What’s the biggest problem with sending from a database trigger?

Un trigger del database viene eseguito nella stessa transazione della modifica della riga. Se chiama un provider email/SMS e la transazione fallisce dopo, potresti aver inviato un messaggio reale riguardo a una modifica che non è rimasta, oppure potresti bloccare la transazione a causa di una chiamata esterna lenta.

What is the outbox pattern in plain terms?

Il pattern outbox memorizza l’intenzione di inviare come una riga nel database, nella stessa transazione della modifica di business. Dopo il commit, un worker legge le righe outbox in sospeso, invia il messaggio e lo marca come inviato, rendendo più sicuri i tempi e i retry.

What should I do when an email/SMS provider request times out?

Spesso l’esito reale è “sconosciuto”, non “fallito”. Un buon sistema registra il tentativo, mette in pausa e ritenta in modo sicuro usando la stessa identità del messaggio, invece di inviare subito un’altra volta e rischiare un duplicato.

How do I prevent duplicate emails or SMS when retries happen?

Usa l’idempotenza: assegna a ogni notifica una chiave stabile che rappresenti cosa significa il messaggio (non quando è stato tentato). Memorizza quella chiave in un registro (spesso la tabella outbox) e fai rispettare un solo record attivo per chiave, così i retry completano lo stesso messaggio invece di crearne uno nuovo.

Which errors should I retry vs treat as permanent?

Ritenta gli errori temporanei come timeout, risposte 5xx o limiti di rate (con attesa). Non ritentare errori permanenti come indirizzi non validi, numeri bloccati o hard bounce; marchiali come falliti e rendili visibili in modo che qualcuno possa correggere i dati invece di ripetere i tentativi.

How do background workers handle restarts or crashes mid-send?

Un worker in background può scansionare i job bloccati in sending oltre una soglia ragionevole, spostarli di nuovo in retryable e ritentarli con backoff. Questo funziona solo se ogni job registra lo stato (tentativi, timestamp, ultimo errore) e l’idempotenza impedisce doppi invii.

What job data do I need to make notification delivery observable?

Devi poter rispondere alla domanda “è sicuro ritentare?”. Memorizza stati chiari come pending, processing, sent e failed, oltre al conteggio dei tentativi e all’ultimo errore. Questo rende il supporto e il debug praticabili e permette al sistema di recuperare senza indovinare.

How would I implement this pattern in AppMaster?

Modella una tabella outbox nel Data Designer, scrivi l’aggiornamento di business e la riga outbox in un’unica transazione, poi esegui la logica di invio e retry in un processo background separato. Mantieni una chiave di idempotenza per ogni messaggio e registra i tentativi, così deploy, retry e riavvii del worker non creano duplicati.

Facile da avviare
Creare qualcosa di straordinario

Sperimenta con AppMaster con un piano gratuito.
Quando sarai pronto potrai scegliere l'abbonamento appropriato.

Iniziare