Pattern da produzione: Outbox, Saga e migrazione incrementale 🧰
Questa guida è per quando sei passato dal “che bello, gli eventi!” al “ok, ma come lo faccio senza perdere dati?”.
Transactional Outbox: il problema del dual-write 🧩
flowchart LR
S[Service] -->|1. tx| DB[(Database)]
DB -->|2. insert| O[(Outbox table)]
PUB[Outbox publisher] -->|poll| O
PUB -->|publish| B[(Broker / Stream)]
Problema classico:
- scrivi su DB (transazione OK)
- pubblichi evento sul broker
Se tra (1) e (2) muori male (crash, rete, timeout), DB e broker si desincronizzano.
Soluzione: Outbox 📦
L’idea è semplice e un po’ “noiosa” (quindi funziona): invece di provare a pubblicare eventi dopo la transazione sperando di non morire tra un passo e l’altro, rendi l’evento parte del commit. Poi pubblichi in modo affidabile, con retry, senza inventarti magie distribuite.
- nella stessa transazione del DB, scrivi anche un record in una tabella outbox;
- un publisher separato legge l’outbox e pubblica sul broker;
- dopo publish, marca il record come inviato.
Risultato: niente eventi “fantasma” e niente aggiornamenti senza evento.
Saga: transazioni distribuite senza illusioni ACID 🎢
flowchart LR
E1[OrdineCreato] --> I[Riserva magazzino]
I --> OKI{OK?}
OKI -->|Sì| P[Autorizza pagamento]
OKI -->|No| C1[Compensa: annulla ordine]
P --> OKP{OK?}
OKP -->|Sì| CONF[Conferma ordine]
OKP -->|No| C2[Compensa: rilascia magazzino]
Quando un processo coinvolge più servizi, evitare la tentazione di fare “2PC” ovunque. Il Two-Phase Commit introduce un coordinatore come SPOF e un protocollo bloccante: nei sistemi distribuiti moderni tende a ridurre disponibilità e aumentare latenza in modo sproporzionato rispetto ai benefici.
Una Saga modella il processo come:
- una sequenza di passi;
- ogni passo pubblica un evento o riceve un comando;
- in caso di fallimento, scattano compensazioni (rollback di business).
Esempio concettuale:
OrdineCreato→ riserva magazzinoMagazzinoRiservato→ autorizza pagamento- se pagamento fallisce →
RilascioMagazzino
Il punto non è “tornare indietro perfetto”, ma preservare integrità di business e consistenza eventuale.
Choreography-based Saga vs Orchestration-based Saga 🎭
Le Saga si implementano in due varianti principali — la stessa distinzione trattata nella guida sui pattern:
-
Choreography-based Saga: ogni servizio reagisce agli eventi e pubblica nuovi eventi per il passo successivo. Non esiste un punto centrale di controllo. Pro: decentralizzata, coupling ridotto. Contro: il flusso di business è distribuito tra più servizi e può diventare difficile da tracciare senza strumenti di observability.
-
Orchestration-based Saga: un orchestratore (spesso un workflow engine o un servizio dedicato) dirige esplicitamente ogni passo — emette comandi, aspetta esiti, gestisce timeout e compensazioni. Pro: visibilità del processo, gestione centralizzata degli errori. Contro: l’orchestratore diventa un componente critico e introduce più coupling.
La scelta tra le due varianti non è ovvia: usa la choreography per processi semplici con side effects indipendenti, l’orchestration quando hai bisogno di controllo esplicito su timeout, retry e compensazioni coerenti.
Migrazione incrementale 🌱
Migrare a EDA “big bang” è un ottimo modo per collezionare aneddoti tragici.
Un pattern efficace per la migrazione graduale è lo Strangler Fig (Martin Fowler): si introduce un layer intermedio (proxy o facade) che intercetta le richieste e instrada il traffico verso il sistema legacy e/o il nuovo sistema event-driven. Man mano che le funzionalità vengono migrate, il routing si sposta progressivamente verso il nuovo sistema, finché il legacy può essere spento senza big bang.
flowchart LR
CLIENT[Client] --> PROXY[Proxy / Facade]
PROXY -->|funzionalità migrate| NEW[Nuovo sistema EDA]
PROXY -->|funzionalità legacy| OLD[Sistema legacy]
NEW -->|eventi| BROKER[(Broker)]
Approccio incrementale tipico:
- pubblica eventi dal sistema esistente (senza cambiare il core path);
- aggiungi consumer nuovi che fanno side effects in parallelo;
- osserva, stabilizza, aggiungi governance;
- migra funzionalità pezzo per pezzo tramite il layer proxy, spegnendo integrazioni legacy.
Pubblicare eventi anche senza consumer (all’inizio) 🚦
Sì, può avere senso pubblicare eventi prima che esistano consumer reali: ti permette di stabilizzare naming, schema e metadata, e di capire il volume reale del traffico. L’importante è non barare: anche in questa fase servono ownership, retention chiara e monitoraggio minimo, altrimenti stai solo accumulando debito tecnico con entusiasmo.
Controlli pratici da mettere subito in piedi:
- monitor del volume e alert su spike inattesi;
- retention minima e policy chiare per evitare accumulo inutile;
- ownership definita fin da subito (anche se i consumer non esistono ancora).
Test pratico prima di abilitare il replay: pubblica eventi in staging, verifica che i consumer di test siano idempotenti e che il tempo di replay non generi load imprevisto sui sistemi downstream.
Rollback strategy 🪂
Sembra noioso, quindi spesso viene saltato. Poi diventa improvvisamente interessante.
- Mantieni per un periodo sia il flusso legacy che quello event-driven.
- Definisci quando un consumer può essere disattivato senza perdere integrità.
- Verifica idempotenza prima di abilitare replay.
Checklist “da produzione” ✅
Questa checklist non ti rende “enterprise-ready”, ma ti evita gli errori più classici quando EDA incontra dati reali e failure reali.
Outbox e dual-write
- Hai un piano per dual-write? (Outbox o equivalente — non “scrivo su DB e poi sul broker sperando che vada”)
- L’Outbox publisher gestisce failure con retry e backoff?
- I record Outbox “inviati” vengono eliminati o archiviati con una policy di retention esplicita?
Saga e compensazioni
- Hai un modello per fallimenti multi-step? (Saga + compensazioni documentate)
- Le compensazioni sono idempotenti? (possono essere invocate più volte senza danni)
- Hai scelto esplicitamente tra choreography e orchestration — e sai perché?
- Esiste un timeout su ogni passo della Saga? I passi “bloccati” vengono rilevati e gestiti?
Migrazione incrementale
- Stai usando un layer proxy/facade (Strangler Fig) per isolare il legacy?
- Hai metriche separate per traffico legacy e traffico EDA, così puoi capire quando è sicuro spegnere l’uno?
- Il rollback è documentato: sai come tornare al flusso legacy se qualcosa va storto?
- Se pubblichi eventi prima che esistano consumer, hai retention, ownership e monitoring già attivi?
Osservabilità e operatività
- Hai metriche e alert su lag e DLQ?
- Ogni evento ha
event_id,correlation_idetimestampnei metadata? - Hai previsto replay — e hai verificato che i consumer siano idempotenti prima di abilitarlo?
- Hai testato il replay in staging misurando il load che genera sui sistemi downstream?
Prossimi passi 🚀
Se ti stai avvicinando a EDA in modo incrementale, il prossimo collo di bottiglia diventa quasi sempre il contratto: naming, schema evolution e governance.
- Per contratti e schema evolution: vedi la guida su event design.
- Per governance e catalogo eventi: vedi la guida di governance.