GH GambleHub

Pattern Outbox

Outbox è un pattern architettonico in cui il servizio di dominio registra le modifiche aziendali e l'evento corrispondente in una singola transazione locale nel proprio archivio. La pubblicazione di un evento in un bus/coda esterno viene eseguita in modo asincrono da un processo sicuro (publisher) che legge la tabella outbox e riproduce i record. Questo approccio elimina la corsa «prima nel database, poi nel pneumatico» e fornisce una consegna affidabile anche in caso di guasti.

1) Quando applicare

Adatto:
  • Microservizi e monoliti modulari con eventi tra contesti.
  • Assicurarsi che «lo stato registrato non possa perdersi».
  • Abbiamo bisogno di idipotenza e di una ricollocazione controllata.
Non è adatto:
  • Le transazioni globali più rigide su più risorse sono critiche (meglio del TSS/saga con contratti espliciti).
  • Nessuna origine di verità selezionata (lo state non è memorizzato dove viene generato l'evento).

2) Obiettivi e proprietà

Voce di dominio + outbox in una singola transazione.
Pubblicazione at-least-once: consentiamo la ripetizione, escludiamo la perdita.
Idemotia dei consumatori: protezione contro le riprese al fianco degli abbonati.
Efficiente exactly-once è raggiunto dalla combinazione outbox + idempotent consumer + dedup.
Telemetria chiara: correlazione tra attività aziendali ed eventi.

3) Schema dati (esempio)

sql
-- Domain table (example: orders)
CREATE TABLE orders (
id       UUID PRIMARY KEY,
tenant_id    TEXT NOT NULL,
status     TEXT NOT NULL,
total_amount  NUMERIC(12,2) NOT NULL,
updated_at   TIMESTAMP NOT NULL DEFAULT now()
);

-- Outbox
CREATE TABLE outbox (
id       UUID PRIMARY KEY,        -- event_id aggregate_type TEXT NOT NULL,          -- 'order'
aggregate_id  UUID NOT NULL,          -- order_id tenant_id    TEXT NOT NULL,
type      TEXT NOT NULL,          -- 'OrderCreated'
payload JSONB NOT NULL, -- serialized headers event JSONB NOT NULL DEFAULT '{}':: jsonb,
occurred_at TIMESTAMP NOT NULL, -- time in domain transaction available_at TIMESTAMP NOT NULL, -- earliest publish time (backoff)
published_at TIMESTAMP, - is filled by the attempts INT NOT NULL DEFAULT 0,
error      TEXT
);

CREATE INDEX ON outbox (available_at) WHERE published_at IS NULL;
CREATE INDEX ON outbox (tenant_id, available_at) WHERE published_at IS NULL;

4) Modello transazionale (application layer)

pseudo begin tx domainChange () # INSERT/UPDATE in domain table insert into outbox (event) # event with aggregate/tenant commit tx keys

Se il commit ha successo, l'evento in outbox è garantito. Se l'app cade dopo la committenza, il pub lo raggiungerà.

5) Pablisher (reader → publisher)

Attività:
  • Leggi periodicamente gli eventi non pubblicati ('published _ at IS NULL' e 'available _ at <= now ()'), con batch.
  • Prova a pubblicare su bus/coda; al successo, segnare «published _ at».
  • In caso di errore, ingrandisci «attemps», metti «available _ at» sul futuro (exponential backoff), scrivi «error».
  • Mantieni i limiti dei tenanti/chiavi (fairness), non blocchi i .
Pseudocode:
pseudo loop:
events = select from outbox where published_at is null and available_at <= now()
order by occurred_at limit BATCH_SIZE for update skip locked

for e in events:
try:
broker. publish(topicFor(e), serialize(e. payload), headers(e))
markPublished(e. id, now())
except Retryable:
backoff = computeBackoff(e. attempts)
reschedule(e. id, now()+backoff, attempts+1, last_error)
except NonRetryable:
moveToDLQ (e) or markError (e) # by sleep (POLL_INTERVAL) policy
💡 'FOR UPDATE SKIP LOCKED'elimina la concorrenza dei pub.

6) Idampotenza e deduplicazione

Dal lato del consumatore (Inbox/Idempotency store):
sql
CREATE TABLE inbox (
consumer_name  TEXT,
event_id    UUID,
processed_at  TIMESTAMP NOT NULL,
PRIMARY KEY (consumer_name, event_id)
);

Algoritmo: quando si riceve un evento, prima si tenta INSERT inbox; se il conflitto chiave è un evento già elaborato con «no-op». Poi c'è la logica aziendale.

Dal lato del pub: «Idempotency-Key» negli headers (ad esempio, «event _ id»), in modo che il bus/broker/proxy possa filtrare i duplicati.

7) Ordine e causalità

L'ordine locale «aggregate _ id» è garantito dall'ordinamento «occurred _ at» e dalla pubblicazione «chiave».
Per i pneumatici di loga con partitura - Partizionare con la chiave «aggregate _ id »/« tenant _ id» in modo che gli eventi di un unico aggregato siano in un unico partigiano.
Se l'ordine è critico, evitare le corse tra i thread del pub con una sola chiave.

8) CDC (Change Data Capture)

Al posto del pub attivo, è possibile utilizzare il CDC: il motore legge il registro delle transazioni del database e trasmette le righe outbox al bus. I vantaggi sono il minimo carico sul database, la sequenza esatta, la mancanza di mezza linea. Gli svantaggi sono complicati da operare e allacciati su specifici database. Entrambi gli approcci sono validi; scegli per competenza e SLO.

9) Errori, DLQ e redrave

Retryable (rete, limiti) - Aumentiamo «attemps», rimandiamo «available _ at» (exponential backoff + jitter).
Non-retryable (nefalide/contratto) - Viene trasferito in DLQ/Dead-Letter Topic con metadati ricchi.
Ridrave sicuro: batch, rate-limit, convalida dello schema, priorità inferiore al traffico prod.

10) Multi-tenenza e limiti

I tag obbligatori sono «tenant _ id», «plan», «region», «outbox». headers`.
Per-tenant fairness: il pub distribuisce «finestre» di pubblicazione e limiti di tentativi agli affittuari.
Residency: memorizza l'outbox nella stessa regione in cui si trovano i dati di dominio; pubblicazione interregionale - solo aggregazioni/riepiloghi.

11) Sicurezza e conformità

Redazione PII in payload/headers sulla politica tenante/regione.
Firma/crittografia del carico utile se il pneumatico è estraneo.
Controllo di tutte le transizioni di stato: creato, pubblicato, errore, redrave.

12) Osservabilità

Metriche:
  • «now - occurred _ at» p50/p95/p99.
  • Percentuale di successo, percentuale di errori, distribuzione delle cause.
  • Dimensioni outbox (non pubblicate), tentativi/secondi.
  • Grafici per tenanti throughput e lag.
Tracing:
  • Correlazione «event _ id »/« aggregate _ id »/« saga _ id»; span db-tx, publish, retry.
  • Annotazioni: «attempt», «backoff _ ms», «dlq = true».
Loghi:
  • Brevi voci di successo; dettagli completi per errore/redrave.

13) Test e caos

Test Atomicity: «Cadiamo» artificialmente dopo aver effettuato una transazione di dominio prima della pubblicazione. L'evento deve uscire in un secondo momento.
Test duplicato - Pubblichiamo lo stesso event più volte - Il consumer esegue esattamente un effetto (inbox).
Test Order: pacchetto di eventi per unità - Verifica sequenza/idampotenza.
Chaos: l'interruzione del broker, l'aumento della latitanza del database, split-brain pub, clock-skew.

14) Modelli di configurazione (esempio)

yaml outbox:
poll_interval_ms: 200 batch_size: 200 order_by: occurred_at backoff:
strategy: exponential_full_jitter initial_ms: 250 max_ms: 10_000 max_attempts: 20 fairness:
per_tenant_parallelism: 4 per_key_serial: true

publisher:
rate_limit_per_sec: 500 headers:
idempotency_key: event_id schema_version: v3 dlq:
enabled: true topic: myapp. events. dlq include_metadata:
- error
- attempts
- source_table
- tenant_id
- aggregate_id

15) Integrazione con sagre e retrai

Outbox - «trasporto di sicurezza» per le fasi della saga: la transazione locale scrive l'effetto e il comando/evento; la pubblicazione è affidabile e dosabile.
I criteri di ripetizione e backoff devono essere coerenti con Retry-After e Circuito Breaker; Evitate la tempesta retraica.

16) Errori tipici

Scrivono un evento dopo una commessa di stato di dominio - possibile perdita in caso di caduta.
Nessun indice/archivio in outbox per aumentare il ritardo di pubblicazione.
Pabliser senza «SKIP LOCKED» o senza charding - concorrenza e blocco.
La mancanza di idepotenza nei consumatori - prese ed effetti collaterali.
Miscelazione PII senza maschera in DLQ/Logi.
Unica coda di pubblicazione globale senza fairness - Tenente rumoroso frena tutti.
La mancanza di monitoraggio del raggio è un degrado nascosto.

17) Scelta rapida della strategia

Livello di partenza: polling da database, batch da 100 a 500, full-jitter backoff, inbox da consumatori.
Alto carico di lavoro: CDC dal registro delle transazioni, sharding su tenant _ id/aggregate _ id, WFQ sugli affittuari.
Ordine rigoroso per aggregazione: pubblicazione di serie per key (mutex), partizionamento del topic con chiave.
Compendio/PII: crittografia payload, redazione in DLQ, outbox regionale.

18) Foglio di assegno prima della vendita

  • Le modifiche di dominio e la scrittura in outbox avvengono in una singola transazione.
  • Il Pablisher elabora i batch, utilizza «SKIP LOCKED», backoff con jitter e limiti.
  • I concettori sono idipotenti (tabella «inbox »/registro di deduzione).
  • DLQ configurato e redrave sicuro.
  • Metriche di laga/errore e alert alle soglie p95/p99.
  • L'ordine della chiave è garantito (partenze/serie).
  • Archivio/riscossione «outbox» e pulizia delle voci pubblicate.
  • Criteri PII e controllo delle transizioni di stato.
  • Test di caduta tra commit e pubblicazione, duplicati e ordine.
  • Documentazione dei contratti evento (schemi/versioni/compatibilità).

Conclusione

Il pattern Outbox trasforma il legamento «fragile» DB in una catena di montaggio affidabile: fissazione atomica dello stato, pubblicazione garantita (anche se «almeno una volta»), follower idipotenti e redrave controllata. Con la giusta telemetria, i limiti e la disciplina dei diagrammi, fornisce un comportamento effettivo exactly-once, riducendo la complessità delle transazioni distribuite e migliorando la resistenza del sistema ai guasti e ai picchi di carico.

Contact

Mettiti in contatto

Scrivici per qualsiasi domanda o richiesta di supporto.Siamo sempre pronti ad aiutarti!

Telegram
@Gamble_GC
Avvia integrazione

L’Email è obbligatoria. Telegram o WhatsApp — opzionali.

Il tuo nome opzionale
Email opzionale
Oggetto opzionale
Messaggio opzionale
Telegram opzionale
@
Se indichi Telegram — ti risponderemo anche lì, oltre che via Email.
WhatsApp opzionale
Formato: +prefisso internazionale e numero (ad es. +39XXXXXXXXX).

Cliccando sul pulsante, acconsenti al trattamento dei dati.