Executive summary
Quando un sistema informativo cresce fino a comprendere decine di componenti indipendenti che devono reagire in tempo reale ai cambiamenti, un ordine confermato, un pagamento ricevuto, una soglia di scorte superata, il modo in cui questi componenti comunicano tra loro diventa il fattore che determina la reattività, l'affidabilità e la capacità di evoluzione dell'intero sistema. Questo articolo analizza le architetture che organizzano la comunicazione tra servizi attorno alla produzione e al consumo di notifiche di cambiamento, esaminando come si memorizzi la storia completa dei cambiamenti anziché solo lo stato corrente, come si separino le operazioni di scrittura da quelle di lettura per ottimizzarle indipendentemente, come le piattaforme di trasporto dei messaggi garantiscano che nessuna notifica vada persa, e come si coordino operazioni che coinvolgono più servizi senza richiedere un controllo centralizzato. L'analisi mostra che queste architetture offrono vantaggi significativi in termini di disaccoppiamento e scalabilità, ma richiedono un ripensamento profondo delle garanzie di coerenza dei dati e della gestione delle transazioni, con sfide operative, dalla evoluzione degli schemi degli eventi alla ricostruzione dello stato dopo un guasto, che la letteratura accademica e l'esperienza industriale stanno ancora affrontando.
Background
L'architettura event-driven (EDA) capovolge il paradigma request-response che ha dominato i sistemi distribuiti per decenni. In un sistema request-response, il componente che necessita di un'azione invoca direttamente il servizio responsabile, creando un accoppiamento temporale e spaziale: il chiamante deve conoscere l'indirizzo del servizio, attendere la risposta e gestire il fallimento della comunicazione. In un'architettura event-driven, i componenti comunicano producendo e consumando eventi, record immutabili che descrivono fatti accaduti nel dominio, attraverso un intermediario (message broker) che disaccoppia produttori e consumatori sia nel tempo sia nello spazio [1]. Questa inversione di dipendenza ha implicazioni architetturali profonde: i produttori non necessitano di conoscere l'identità né il numero dei consumatori, e i consumatori possono essere aggiunti, rimossi o temporaneamente indisponibili senza impatto sulla sorgente degli eventi.
Il fondamento teorico di questa architettura affonda le radici nel concetto di log come astrazione unificante. Kleppmann [1] ha formalizzato la visione secondo cui ogni database è una materializzazione derivata di un log append-only di eventi: il log è la sorgente primaria di verità, e tabelle, indici e cache sono proiezioni calcolate a partire da esso. Questa prospettiva, anticipata da Kreps nel 2013 con la proposta del log distribuito come primitiva fondamentale per l'integrazione dati [2], ha rovesciato la relazione tradizionale tra database e replica, rendendo esplicito ciò che ogni sistema relazionale mantiene implicitamente nel Write-Ahead Log. Helland [3], in un contributo seminale presentato a CIDR 2007, aveva già argomentato che le transazioni distribuite non scalano oltre i confini di una singola entità, e che i sistemi su larga scala richiedono approcci alternativi basati su attività compensative e garanzie di consistenza rilassate, una tesi che anticipava l'adozione dei pattern che questo articolo analizza.
Il teorema CAP, dimostrato formalmente da Gilbert e Lynch [4], stabilisce l'impossibilità per un sistema distribuito di garantire simultaneamente consistenza forte, disponibilità e tolleranza alle partizioni di rete. In un'architettura event-driven, la scelta di progetto ricade tipicamente sulla disponibilità e la tolleranza alle partizioni, accettando una consistenza eventuale (eventual consistency) tra i servizi. Questa scelta non è una concessione, ma una necessità strutturale: in un sistema composto da decine di microservizi con database indipendenti, il mantenimento di consistenza forte richiederebbe protocolli di commit distribuito (2PC) la cui latenza e fragilità sono incompatibili con i requisiti di throughput e resilienza delle applicazioni moderne [3]. La survey di Laigner et al. [5], basata su un'analisi sistematica della letteratura e un questionario rivolto a oltre 120 professionisti, ha confermato che la gestione della consistenza dei dati tra microservizi è la sfida più critica percepita sia dalla comunità accademica sia dall'industria, e che i pattern event-driven, event sourcing, CQRS e saga, rappresentano le soluzioni più adottate.
Architettura dei message broker
Il message broker è il componente infrastrutturale che implementa il disaccoppiamento tra produttori e consumatori di eventi. La scelta del broker determina le garanzie di ordinamento, durabilità, throughput e latenza disponibili all'architettura, e non è una decisione fungibile: broker diversi incarnano modelli semantici fondamentalmente diversi.
Log distribuito vs. coda tradizionale
La distinzione architetturale primaria è tra il modello a log distribuito e il modello a coda di messaggi. Apache Kafka [6], il sistema che ha definito il paradigma del log distribuito per l'event streaming, organizza gli eventi in topic partizionati dove ogni partizione è un log append-only ordinato. I consumatori mantengono un offset che indica la posizione di lettura nel log; gli eventi non vengono rimossi dopo il consumo, ma restano disponibili per la durata della retention configurata, o indefinitamente con log compaction. Questo design consente il replay degli eventi, una proprietà fondamentale per l'event sourcing e per la ricostruzione di read model in CQRS. La systematic review di Mohammad [7], che ha analizzato 42 studi peer-reviewed pubblicati tra il 2015 e il 2025, ha identificato nove pattern architetturali ricorrenti negli ecosistemi Kafka, tra cui event sourcing replay, CQRS bus e saga orchestrator, evidenziando come il log distribuito sia diventato la primitiva su cui si costruiscono i pattern di livello applicativo.
Il modello a coda tradizionale, rappresentato da RabbitMQ, implementa una semantica diversa: i messaggi vengono consegnati a un consumatore e rimossi dalla coda dopo l'acknowledgment. Questo modello è ottimale per pattern di work distribution e task scheduling, dove ogni messaggio deve essere processato esattamente da un consumer, ma non supporta nativamente il replay né l'ordinamento globale. Apache Pulsar adotta un'architettura ibrida che separa compute (broker) e storage (Apache BookKeeper), supportando sia la semantica pub/sub sia quella di coda esclusiva, con tiered storage che consente la retention a lungo termine su object storage [8]. La scelta tra questi modelli non è una questione di performance grezza, Kafka raggiunge throughput superiori a 1.2 milioni di messaggi al secondo con latenza P95 di 18ms in condizioni ottimali [7], mentre Pulsar offre throughput comparabili con vantaggi architetturali nel multi-tenancy e nella geo-replicazione [8], ma di allineamento semantico con i pattern applicativi richiesti.
Garanzie di consegna e exactly-once semantics
Le garanzie di consegna di un message broker si articolano su tre livelli: at-most-once (il messaggio può andare perso), at-least-once (il messaggio può essere duplicato) e exactly-once (ogni messaggio viene processato esattamente una volta). La semantica exactly-once, a lungo considerata irrealizzabile in sistemi distribuiti, è stata implementata in Kafka a partire dalla versione 0.11 attraverso la combinazione di producer idempotenti (che eliminano i duplicati tramite sequence number per partizione) e transazioni (che garantiscono l'atomicità di scritture su topic multipli) [6]. Il costo di questa garanzia non è trascurabile: l'abilitazione dell'exactly-once in Kafka Streams può degradare significativamente la latenza di recovery in caso di failure, in funzione del data rate e della topologia della pipeline [9]. La scelta del livello di garanzia è dunque un trade-off esplicito tra correttezza e performance che deve essere valutato nel contesto del dominio applicativo.
Event sourcing
L'event sourcing è un pattern architetturale in cui lo stato di un'entità di dominio non viene memorizzato come record mutabile, ma ricostruito dal replay di una sequenza ordinata di eventi immutabili che rappresentano i fatti accaduti. Fowler [10] ha descritto il pattern nel 2005, e Young ne ha formalizzato la relazione con CQRS e Domain-Driven Design nel contesto dei sistemi a domini complessi [11]. La differenza rispetto a un database CRUD è strutturale: in un sistema CRUD, un aggiornamento sovrascrive lo stato precedente, perdendo la storia delle transizioni; in un sistema event-sourced, il log degli eventi è la sorgente di verità, e lo stato corrente è una proiezione derivata.
Architettura dell'event store
L'event store è un database specializzato ottimizzato per operazioni append-only. Ogni stream di eventi corrisponde a un'entità di dominio (un ordine, un conto, un dispositivo) ed è identificato da un aggregate ID. Le operazioni di scrittura aggiungono eventi allo stream con concorrenza ottimistica basata sul numero di versione: se due scritture concorrenti tentano di appendere un evento alla stessa versione, una delle due fallisce, forzando il retry con la logica di conflitto del dominio. Le operazioni di lettura caricano l'intera sequenza di eventi e la riproducono (replay) attraverso una funzione di fold per ricostruire lo stato corrente:
$$S_n = f(S_0, e_1, e_2, \ldots, e_n)$$
dove $S_0$ è lo stato iniziale, $e_i$ è l'i-esimo evento e $f$ è la funzione di applicazione che, dato uno stato e un evento, produce il nuovo stato. Questa formulazione rende il sistema deterministico e riproducibile: dato lo stesso log di eventi, lo stato ricostruito è identico indipendentemente dal momento in cui il replay viene eseguito.
Proiezioni, snapshot e sfide operative
Il replay dell'intero log per ogni lettura è computazionalmente proibitivo quando gli stream contengono migliaia o milioni di eventi. Due meccanismi mitigano questo problema. Le proiezioni (read model) sono materializzazioni dello stato ottimizzate per specifici pattern di query, costruite consumando il flusso di eventi in modo asincrono; la separazione tra il modello di scrittura (event log) e i modelli di lettura (proiezioni) è il punto di congiunzione naturale tra event sourcing e CQRS. Gli snapshot sono salvataggi periodici dello stato corrente che consentono di avviare il replay dall'ultimo snapshot anziché dall'inizio dello stream, riducendo il costo di ricostruzione a un fattore proporzionale al numero di eventi successivi allo snapshot anziché al totale.
Lo studio empirico di Overeem et al. [12], condotto su 19 sistemi event-sourced in produzione con il contributo di 25 ingegneri, ha identificato cinque sfide ricorrenti che la letteratura teorica sottostima. La ricostruzione delle proiezioni, necessaria quando la logica di proiezione cambia o quando si aggiunge un nuovo read model, richiede il riprocessamento dell'intero log storico, un'operazione che in sistemi con milioni di eventi può richiedere ore. L'evoluzione degli schemi degli eventi è particolarmente insidiosa: poiché gli eventi sono immutabili, un cambiamento nella struttura di un evento non può essere applicato retroattivamente, e i cinque metodi documentati, versioned events, weak schema, upcasting, in-place transformation e copy-and-transform [12], presentano ciascuno trade-off significativi tra complessità operativa, performance e correttezza. La conformità al GDPR rappresenta una tensione strutturale con il principio di immutabilità: il diritto alla cancellazione richiede meccanismi come la crypto-shredding (cifratura dei dati personali con chiavi per-utente, con cancellazione della chiave equivalente alla cancellazione logica del dato) che aggiungono complessità crittografica all'event store.
CQRS: separazione di comandi e query
Il pattern CQRS (Command Query Responsibility Segregation) generalizza il principio CQS (Command Query Separation) di Meyer, secondo cui ogni metodo deve essere o un comando che modifica lo stato o una query che restituisce dati, ma non entrambi, dal livello del singolo oggetto al livello architetturale [13]. In un sistema CQRS, il modello di scrittura (command side) e il modello di lettura (query side) sono separati fisicamente, con strutture dati, storage e percorsi di scaling indipendenti.
Motivazione architetturale
La separazione è motivata dall'asimmetria fondamentale tra i carichi di lettura e scrittura nella maggior parte delle applicazioni. Il command side gestisce la validazione delle regole di business, l'applicazione dei vincoli di dominio e la persistenza delle mutazioni; il query side serve le interrogazioni, spesso con pattern di accesso radicalmente diversi (aggregazioni, ricerche full-text, join complessi) che richiedono strutture dati ottimizzate per la lettura. In un modello unificato, lo schema del database è un compromesso che non ottimizza né le scritture né le letture. CQRS elimina questo compromesso: il command side può utilizzare un event store o un database relazionale normalizzato, mentre il query side può mantenere viste materializzate, indici denormalizzati o database specializzati (Elasticsearch per la ricerca testuale, un data warehouse colonnare per le analisi).
La separazione consente ottimizzazioni significative sulle operazioni di lettura: il read model può essere denormalizzato e indicizzato specificamente per i pattern di query dell'applicazione, eliminando join complessi e riducendo la latenza delle interrogazioni in modo proporzionale alla specializzazione dello schema [14]. Tuttavia, la separazione introduce complessità architetturale non trascurabile: il read model è aggiornato in modo asincrono dal command side, tipicamente attraverso il consumo di eventi, e presenta quindi una finestra di inconsistenza temporale (stale read). La gestione di questa eventual consistency nel front-end e nelle API richiede strategie esplicite: read-your-own-writes (garantire che l'utente che ha eseguito la scrittura veda il proprio aggiornamento), causal consistency (garantire che gli eventi correlati siano visibili nell'ordine causale corretto), o versioning esplicito degli aggiornamenti.
CQRS e event sourcing: complementarietà e indipendenza
CQRS e event sourcing sono spesso descritti come un unico pattern, ma sono architetturalmente indipendenti. È possibile implementare CQRS senza event sourcing (il command side persiste lo stato in un database relazionale e pubblica eventi di cambiamento per aggiornare il read model) e implementare event sourcing senza CQRS (un singolo modello serve sia scritture sia letture). Tuttavia, la combinazione dei due pattern è sinergica: l'event store fornisce il log canonico da cui le proiezioni CQRS vengono derivate, e la separazione dei modelli di lettura e scrittura risolve il problema principale dell'event sourcing, la difficoltà di eseguire query complesse direttamente sull'event log. La systematic review di Mohammad [7] ha rilevato che il pattern "CQRS bus" su Kafka è uno dei nove pattern architetturali più ricorrenti nell'ecosistema event streaming, confermando la prevalenza di questa combinazione nell'industria.
Eventual consistency e modelli di consistenza
In un'architettura event-driven, la consistenza tra i servizi è eventuale per definizione: quando un servizio pubblica un evento, i consumatori lo riceveranno e aggiorneranno il proprio stato, ma esiste un intervallo di tempo, da millisecondi a secondi, durante il quale i diversi servizi hanno visioni divergenti dello stato del sistema. Come conseguenza dell'impossibilità formale stabilita dal teorema CAP [4], l'architettura event-driven privilegia la disponibilità, accettando che le letture possano restituire dati non ancora convergenti.
Livelli di consistenza e implicazioni pratiche
La consistenza eventuale non è un modello monolitico, ma un gradiente. Il livello più debole è l'eventual consistency pura: l'unica garanzia è che, in assenza di nuove scritture, tutti i nodi convergeranno eventualmente allo stesso stato. Modelli intermedi offrono garanzie più forti mantenendo la disponibilità: la causal consistency garantisce che se un evento B è causalmente dipendente da un evento A, ogni consumatore che osserva B ha già osservato A; la read-your-own-writes consistency garantisce che un processo che ha scritto un valore legga almeno quel valore nelle successive letture; la monotonic read consistency garantisce che le letture successive di un processo non regrediscano a stati precedenti.
La scelta del modello di consistenza ha implicazioni concrete sulla progettazione dell'applicazione. In un sistema e-commerce event-driven, un ordine confermato pubblica un evento che deve essere consumato dal servizio di inventario (per decrementare le scorte), dal servizio di pagamento (per processare l'addebito) e dal servizio di notifica (per informare il cliente). Se il servizio di inventario processa l'evento prima del servizio di pagamento, esiste una finestra temporale in cui le scorte sono decrementate ma il pagamento non è ancora avvenuto, uno stato intermedio che deve essere gestito esplicitamente nella logica di business. Laigner et al. [15], nel benchmark Online Marketplace presentato a SIGMOD 2025, hanno dimostrato che la gestione corretta di queste finestre di inconsistenza, inclusi vincoli di integrità cross-servizio, query consistenti su dati replicati e processamento di eventi in ordine causale, rappresenta la sfida centrale nella progettazione di applicazioni a microservizi, e che i framework attuali lasciano la responsabilità di queste garanzie quasi interamente allo sviluppatore applicativo.
Il problema dell'idempotenza
In un sistema at-least-once, il livello di garanzia più comune nei message broker, poiché at-most-once rischia la perdita di dati e exactly-once ha un costo di performance, i consumatori possono ricevere lo stesso evento più volte. Ogni consumer deve quindi essere idempotente: il processamento ripetuto dello stesso evento deve produrre lo stesso risultato della prima elaborazione. Le strategie per l'idempotenza includono il deduplication basato su event ID (il consumer mantiene un registro degli ID già processati), il design idempotente per costruzione (l'operazione produce lo stesso stato indipendentemente dal numero di esecuzioni, ad esempio un SET è idempotente mentre un INCREMENT non lo è), e il conditional write (l'aggiornamento è condizionato alla versione corrente dello stato, fallendo silenziosamente se la versione è già stata avanzata da un processamento precedente). La complessità dell'idempotenza è spesso sottostimata: in operazioni con effetti collaterali (invio di email, addebiti su carta di credito), l'idempotenza richiede meccanismi di tracking esterni e non può essere ottenuta puramente dal design dell'operazione [1].
Il saga pattern per le transazioni distribuite
In un'architettura a microservizi con database indipendenti, un'operazione di business che coinvolge più servizi non può essere gestita con una transazione ACID tradizionale: non esiste un transaction manager globale che coordini il commit su database eterogenei senza introdurre accoppiamento, latenza e single point of failure. Il saga pattern, introdotto da Garcia-Molina e Salem nel 1987 [16], fornisce un'alternativa: una saga è una sequenza di transazioni locali, ciascuna eseguita da un singolo servizio, coordinate attraverso eventi o comandi. Se una transazione locale fallisce, la saga esegue transazioni compensative (compensating transactions) per annullare semanticamente gli effetti delle transazioni precedenti.
Orchestrazione vs. coreografia
Le due strategie di coordinamento, orchestrazione e coreografia, incarnano filosofie architetturali opposte. Nella coreografia, ogni servizio pubblica un evento al completamento della propria transazione locale, e il servizio successivo nella sequenza reagisce a quell'evento. Non esiste un coordinatore centrale: la logica della saga è distribuita nei singoli servizi. Il vantaggio è l'assenza di single point of failure e l'indipendenza dei servizi; lo svantaggio è che la logica complessiva della saga è implicita, distribuita nel codice dei singoli servizi, e diventa rapidamente difficile da comprendere e debuggare al crescere del numero di passi. Richardson [14] osserva che la coreografia è praticabile per saghe semplici (3-4 passi), ma che la complessità cognitiva cresce in modo super-lineare con il numero di servizi coinvolti.
Nell'orchestrazione, un componente dedicato (saga orchestrator) gestisce il flusso della transazione, invocando i servizi partecipanti in sequenza e gestendo le compensazioni in caso di fallimento. L'orchestratore mantiene lo stato della saga e implementa la logica decisionale (quale servizio invocare, quando compensare, come gestire i timeout). Il vantaggio è la centralizzazione della logica, che rende il flusso esplicito e testabile; lo svantaggio è l'introduzione di un componente con responsabilità significativa e il rischio di accoppiamento logico tra l'orchestratore e i servizi partecipanti.
Il problema dell'isolamento
La differenza più profonda tra una saga e una transazione ACID è l'assenza di isolamento. In una transazione ACID, l'isolamento garantisce che le transazioni concorrenti non osservino stati intermedi. In una saga, gli effetti di ciascuna transazione locale sono visibili immediatamente dopo il commit, prima che la saga sia completata. Questo significa che altre saghe o query concorrenti possono leggere dati parzialmente aggiornati, un problema noto come dirty read a livello di saga. Daraghmi et al. [17] hanno proposto un'estensione del saga pattern che affronta l'assenza di isolamento tramite un quota cache (uno strato in memoria che mantiene le risorse prenotate dalle saghe in corso) e un commit-sync service che sincronizza il commit delle transazioni locali con l'aggiornamento del cache. I risultati sperimentali su un sistema e-commerce a microservizi mostrano che l'approccio risolve scenari di overselling causati dalla lettura di stati intermedi, con prestazioni superiori alla versione baseline grazie allo spostamento delle operazioni di verifica dal database alla memoria [17].
Compensazione e semantic rollback
Le transazioni compensative non sono un undo meccanico: sono operazioni di dominio che annullano semanticamente gli effetti di una transazione precedente. La cancellazione di un ordine non è un DELETE dal database, ma la creazione di un evento di cancellazione che decrementa l'addebito, ripristina le scorte e genera una notifica. Non tutte le operazioni sono compensabili: l'invio di un'email o l'addebito su un gateway di pagamento esterno richiede strategie specifiche (invio di un'email correttiva, richiesta di rimborso). Garcia-Molina e Salem [16] hanno distinto tra transazioni compensabili (il cui effetto può essere annullato), transazioni pivot (il punto di non ritorno dopo il quale la saga può solo procedere in avanti) e transazioni retriable (che devono eventualmente riuscire, possibilmente con retry). La progettazione corretta della sequenza di transazioni, con le operazioni non compensabili posizionate dopo la transazione pivot, è essenziale per garantire la correttezza della saga.
Pattern di integrazione e architetture di riferimento
L'adozione congiunta dei pattern descritti nelle sezioni precedenti produce architetture composite la cui progettazione richiede decisioni esplicite su molteplici assi.
Outbox pattern e dual write
Il problema del dual write emerge quando un servizio deve atomicamente aggiornare il proprio database e pubblicare un evento sul broker: se il database commit avviene ma la pubblicazione fallisce, il sistema si trova in uno stato inconsistente. L'outbox pattern risolve il problema scrivendo l'evento in una tabella dedicata (outbox) nella stessa transazione del database, e utilizzando un meccanismo di Change Data Capture (CDC), tipicamente Debezium [18], per propagare gli eventi dall'outbox al broker. Poiché la scrittura nel database e la scrittura nell'outbox avvengono nella stessa transazione ACID locale, l'atomicità è garantita dal database stesso senza ricorrere a protocolli distribuiti.
Il diagramma seguente illustra il flusso dell'outbox pattern in una pipeline event-driven.
graph LR
A[Servizio] -->|Transazione locale| B[Database]
A -->|Stessa transazione| C[Tabella Outbox]
C -->|CDC / Polling| D[Debezium Connector]
D --> E[Message Broker<br/>Apache Kafka]
E --> F[Consumer A<br/>Read Model CQRS]
E --> G[Consumer B<br/>Saga Orchestrator]
E --> H[Consumer C<br/>Servizio downstream]
Figura 1. Outbox pattern: la scrittura dell'evento nella tabella outbox avviene nella stessa transazione locale del database, garantendo atomicità senza protocolli distribuiti. Il CDC propaga gli eventi al broker.
Architettura composita: event sourcing + CQRS + saga
In un sistema che combina event sourcing, CQRS e saga, il flusso di una operazione di business multi-servizio segue un percorso definito. Il command side di ciascun servizio persiste le mutazioni come eventi nell'event store. Il broker distribuisce gli eventi ai consumer che aggiornano i read model (CQRS) e al saga orchestrator che coordina la transazione distribuita. Se un passo della saga fallisce, l'orchestratore pubblica eventi di compensazione che vengono processati dai command side dei servizi interessati, generando nuovi eventi compensativi nell'event store. Il read model viene aggiornato di conseguenza, con un ritardo proporzionale alla latenza del broker e al tempo di processamento dei consumer.
graph TB
subgraph "Servizio Ordini"
A[Command Handler] --> B[Event Store]
B --> C[Proiezione<br/>Read Model]
end
B -->|Evento: OrdineCreato| D[Message Broker]
subgraph "Saga Orchestrator"
D --> E[Saga: ProcessoOrdine]
end
E -->|Comando: RiservaScorte| F[Servizio Inventario]
E -->|Comando: ProcessaPagamento| G[Servizio Pagamento]
F -->|Evento: ScorteRiservate| D
G -->|Evento: PagamentoConfermato| D
G -->|Fallimento| E
E -->|Compensazione: RilasciaScorte| F
Figura 2. Architettura composita event sourcing + CQRS + saga. Il saga orchestrator coordina la transazione distribuita consumando e producendo eventi attraverso il broker. Ogni servizio mantiene il proprio event store e i propri read model.
La complessità operativa di questa architettura non è trascurabile. Ogni servizio mantiene il proprio event store e i propri read model; il saga orchestrator mantiene lo stato delle transazioni distribuite; il broker deve garantire ordinamento e durabilità. Il numero di componenti infrastrutturali e le interazioni tra essi crescono in modo combinatorio, e il debugging di una transazione distribuita che attraversa quattro servizi, un orchestratore e tre read model richiede strumenti di observability, distributed tracing, correlation ID, event log consolidati, senza i quali la diagnosi di un problema in produzione è di fatto impossibile.
Limiti, problemi aperti e direzioni future
L'adozione delle architetture event-driven in produzione ha rivelato sfide che la letteratura teorica affronta solo parzialmente.
L'evoluzione degli schemi degli eventi è il problema operativo più insidioso. In un sistema event-sourced, gli eventi sono immutabili per definizione: un cambiamento nella struttura di un evento (aggiunta di un campo, rinomina, modifica del tipo) non può essere applicato retroattivamente al log storico. Overeem et al. [12] hanno documentato che i team in produzione adottano combinazioni dei cinque metodi di schema evolution, versioned events, weak schema, upcasting, in-place transformation, copy-and-transform, ma che nessuno di essi è privo di problemi: il versioning moltiplica la complessità dei consumer, l'upcasting introduce logica di migrazione nel percorso critico di lettura, e il copy-and-transform richiede la duplicazione dell'intero event store. La mancanza di un approccio standardizzato e tool-supported per la schema evolution rimane una delle barriere principali all'adozione dell'event sourcing in contesti enterprise.
Il testing e il debugging delle saghe distribuite presentano sfide specifiche. Una saga con cinque servizi e relative compensazioni ha un numero esponenziale di percorsi di esecuzione possibili (successo, fallimento a ciascun passo, timeout, retry, failure parziale della compensazione). La verifica esaustiva di questi percorsi richiede test di integrazione end-to-end che coinvolgono l'intera infrastruttura, con costi di setup e di esecuzione significativi. I framework per saga come Eventuate Tram e Temporal hanno introdotto meccanismi di replay deterministico e testing isolato dell'orchestratore, ma la verifica del comportamento complessivo del sistema rimane un problema aperto [14].
La consistenza cross-servizio nella pratica è più complessa di quanto i modelli teorici suggeriscano. Il benchmark Online Marketplace di Laigner et al. [15] ha evidenziato che la gestione dei vincoli di integrità distribuiti (ad esempio, il vincolo che un ordine non possa essere confermato se le scorte sono insufficienti), la query consistente su dati replicati tra servizi, e il processamento di eventi in ordine causale corretto sono sfide che i framework attuali delegano quasi interamente allo sviluppatore, senza astrazioni di livello superiore che ne semplifichino la gestione.
La survey di Laigner et al. [5] identifica come direzioni future la necessità di sistemi di data management nativamente orientati ai microservizi, che integrino garanzie di consistenza cross-servizio come primitiva di prima classe, e di benchmark standardizzati che catturino le sfide reali delle architetture event-driven, superando la frammentazione di metriche e configurazioni documentata nella systematic review dell'ecosistema Kafka [7]. L'evoluzione verso architetture serverless event-driven, dove il broker e l'orchestratore sono servizi managed con scaling automatico, riduce la complessità operativa infrastrutturale ma non elimina le sfide architetturali di consistenza, evoluzione degli schemi e testing, che restano responsabilità dell'applicazione.
Riferimenti
[1] M. Kleppmann, Designing Data-Intensive Applications, O'Reilly Media, 2017.
[2] J. Kreps, "The Log: What every software engineer should know about real-time data's unifying abstraction," LinkedIn Engineering Blog, 2013. https://engineering.linkedin.com/distributed-systems/log-what-every-software-engineer-should-know-about-real-time-datas-unifying
[3] P. Helland, "Life beyond Distributed Transactions: an Apostate's Opinion," in Proc. CIDR, 2007. https://ics.uci.edu/~cs223/papers/cidr07p15.pdf
[4] S. Gilbert, N. Lynch, "Brewer's Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services," in ACM SIGACT News, vol. 33, no. 2, 2002. https://dl.acm.org/doi/10.1145/564585.564601
[5] R. Laigner et al., "Data Management in Microservices: State of the Practice, Challenges, and Research Directions," in PVLDB, vol. 14, no. 13, 2021. https://dl.acm.org/doi/abs/10.14778/3484224.3484232
[6] Apache Software Foundation, "Apache Kafka Documentation," 2026. https://kafka.apache.org/documentation/
[7] M. Mohammad, "Analysis of Design Patterns and Benchmark Practices in Apache Kafka Event-Streaming Systems," arXiv:2512.16146, 2025. https://arxiv.org/abs/2512.16146
[8] Apache Software Foundation, "Apache Pulsar Documentation," 2026. https://pulsar.apache.org/docs/
[9] A. Vogel et al., "A Comprehensive Benchmarking Analysis of Fault Recovery in Stream Processing Frameworks," in Proc. ACM DEBS, 2024. https://dl.acm.org/doi/10.1145/3629104.3666040
[10] M. Fowler, "Event Sourcing," martinfowler.com, 2005. https://martinfowler.com/eaaDev/EventSourcing.html
[11] G. Young, "CQRS Documents," 2010. https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf
[12] M. Overeem et al., "An Empirical Characterization of Event Sourced Systems and Their Schema Evolution — Lessons from Industry," in Journal of Systems and Software, vol. 178, 2021. https://arxiv.org/abs/2104.01146
[13] M. Fowler, "CQRS," martinfowler.com, 2011. https://martinfowler.com/bliki/CQRS.html
[14] C. Richardson, Microservices Patterns: With Examples in Java, Manning Publications, 2018. https://microservices.io/book
[15] R. Laigner et al., "Online Marketplace: A Benchmark for Data Management in Microservices," in Proc. ACM SIGMOD, vol. 3, no. 1, 2025. https://dl.acm.org/doi/10.1145/3709653
[16] H. Garcia-Molina, K. Salem, "Sagas," in Proc. ACM SIGMOD, 1987. https://dl.acm.org/doi/10.1145/38713.38742
[17] E. Y. Daraghmi, C. Zhang, S.-M. Yuan, "Enhancing Saga Pattern for Distributed Transactions within a Microservices Architecture," in Applied Sciences, vol. 12, no. 12, 2022. https://www.mdpi.com/2076-3417/12/12/6242
[18] Debezium Community, "Debezium Documentation," 2026. https://debezium.io/documentation/