Vai al contenuto
Produzione

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:

  1. scrivi su DB (transazione OK)
  2. 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 magazzino
  • MagazzinoRiservato → 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:

  1. pubblica eventi dal sistema esistente (senza cambiare il core path);
  2. aggiungi consumer nuovi che fanno side effects in parallelo;
  3. osserva, stabilizza, aggiungi governance;
  4. 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_id e timestamp nei 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.

Ultimo aggiornamento il