Command Query Responsibility Segregation (CQRS)
CQRS è un pattern architetturale presentato da Martin Fowler [1]. L’idea su cui si basa consiste nel dividere il modello concettuale di un’applicazione in due: quello inerente all’aggiornamento e quello riguardante la visualizzazione. I concetti su cui si basa sono:
- Queries che restituiscono un risultato immutabile rispetto allo stato del sistema.
- Commands che permettono di manipolare lo stato senza ritornare nessun valore.
Questa suddivisione permette di passare da un’architettura dove la suddivisione è basata su vari strati (DAO, Service etc.) a una dove la separazione è basata su interrogazioni (Query) e modifiche (Commands)
Un vantaggio di questa suddivisione è la possibilità di avere un costante flusso di modifica dei dati separato dal flusso delle interrogazioni. Un altro vantaggio è riferito alla possibilità di aumentare i carichi di lavoro, incrementando la parte di command rispetto a quella di query o viceversa, in base al reale carico.
Per approfondire questi e altri aspetti del pattern, vi rimando a una serie di articoli [2] pubblicati su MokaByte qualche anno. In questa serie, infatti, ci concentreremo in maniera operativa su un progetto pratico.
Axon
Giunto alla versione 3.3, Axon Framework [3] supporta lo sviluppatore nella fase di implementazione del pattern CQRS, rendendo disponibili i blocchi. I blocchi principali disponibili sono:
- aggregati
- repository
- eventi
Dobbiamo ricordarci che il framewrork non nasconde la complessità di CQRS ma semplicemente dà un aiuto mettendo a disposizione un’API coerente e che nasconde tutte le problematiche di comunicazione di eventi e sincronizzazione.
Il progetto
L’applicazione di esempio permette di creare nuove gift card e di utilizzare il loro valore per simulare l’acquisto di un bene. L’applicazione è divisa in tre macro blocchi:
- un blocco si occupa della gestione dei comandi;
- uno gestisce la parte di interrogazione;
- c’è poi una piccola UI che fa da raccordo ai due blocchi precedenti.
La prima implementazione si basa su un’unica applicazione web basata su Spring Boot, mentre per l’interrogazione dei dati ci appoggeremo a un container Docker con all’interno PostgreSQL. La parte di eventi sarà salvata utilizzando un prodotto specifico, AxonDB [4], nella versione per sviluppatori che è gratuita ma limitata a un numero basso di eventi. In alternativa possiamo utilizzare prodotti specifici o lo stesso PostgreSQL anche per la persistenza degli eventi.
Partiamo subito
Il progetto è reperibile sul mio account GitHub [5] e, per semplificare un po’ l’avvio dell’applicazione (installazione di PostgreSQL / AxonDB), ho preparato un docker-compose per far partire il necessario senza dover impazzire.
C’è però un unico accorgimento da seguire: anche la versione gratuita per sviluppatori di AxonDB necessita di una licenza; quindi, per ottenere il JAR da utilizzare per creare l’immagine Docker, fate riferimento al file README.md presente nel repository.
Command
Nel package com.marco.cqrs.command si trovano le classi che definiscono i comandi e gli eventi. Essendo tutte le modifiche generate da eventi che rispondono a dei comandi impartiti al sistema, una parte importante della fase di progettazione è proprio definire un modello dei dati coerente e definire tutti i comandi necessari.
Nel package troviamo le classi:
- java, IssuedEvt.java: classi che servono a gestire la creazione di una nuova gift card;
- java, RedeemedEvt.java: classi che servono a gestire la il processo di riscatto (redeem) di una gift card creata precedentemente.
Nel package com.marco.cqrs.model, invece, troviamo la classe GiftCard.java che gestisce gli aggregati e gli handler per i comandi e gli eventi che andremo a implementare.
Prendendo in considerazione le classi che modellano comandi ed eventi, la caratteristica che voglio far notare riguarda la classe RedeemCmd.java che contiene l’annotazione @TargetAggregateIdentifier come nel codice che segue
Questa annotazione istruisce il framework (Axon) a considerare il campo come id dell’aggregato, la nostra chiave univoca all’interno dell’Event store.
Catena di dati immutabili
Una caratteristica del pattern CQRS è quella dell’immutabilità dei dati; proprio per questo comandi ed eventi hanno la caratteristica di essere “immutabili”: ogni modifica (command) e ogni evento di modifica generato da un comando deve essere ripetibile all’interno della catena che l’ha generato.
Semplificando, partendo dall’evento 0 e arrivando all’evento n posso ricostruire interamente la struttura dei dati presenti nella parte di view/query. Ricordiamoci quindi di fare il backup dell’event store con regolarità sia per poter ricostruire il valore finale sia per poter verificare eventuali bug.
Aggregati
Gli aggregati sono una entità o un gruppo di entità che devono stare in uno stato consistente. Nel caso di un gruppo di entità esiste sempre un Aggregate Root che ha il compito di vigilare sulla consistenza dello stato; la struttura è tipica nel modello DDD dove un oggetto non è identificato solo dalle sue proprietà ma è definito in maniera puntuale con oggetti specifici.
Concordo che, come esempio, sia un po’ forzato ma è per far passare il concetto che sta dietro alla metodologia DDD.
Per Axon un aggregato è identificato da un Aggregate Identifier, che risulta essere un semplice oggetto; ma, per essere una buona implementazione, deve:
- implementare equals e hasCode;
- implementare toString();
- essere Serializable.
Altra cosa importante è che è meglio utilizzare id univoci generati come UUID: questo per evitare falsi positivi nell’identificazione degli oggetti.
Come detto un Aggregate è un oggetto normale che contiene stato e metodi per alterare il suo stato. Questo va un po’ contro i principi di CQRS, ma nulla vieta di esporre lo stato attraverso metodi di accesso (setter e getter, per intenderci)
Nel nostro caso, la classe GiftCard.java è il nostro Aggregate Root e gestisce lo stato di una gift card molto semplice, dove abbiamo definito l’id e il valore. Inoltre, presenta la gestione dei comandi e degli eventi associati creati precedentemente.
La gestione dello stato dell’oggetto è fatta tramite i metodi annotati con @CommandHandler e @EventSourcingHandler che istruiscono Axon su quali metodi invocare, in base ai comandi o eventi.
In generale, un command handler si occupa di ricevere il comando da elaborare, fare una validazione dello stato dell’oggetto e generare l’evento di modifica, con i valori provenienti dal command. Sarà compito dell event source handler modificare lo stato interno dell’oggetto.
Il vantaggio di utilizzare Axon è quello di non doverci preoccupare di instradare messaggi su code: sarà proprio il nostro framework Axon a occuparsi di nascondere questa complessità, gestendo tutto tramite un suo Event Bus.
Event Bus in Axon
Tale Event Bus si occupa di instradare gli eventi ai listeners corrispondenti in maniera sincrona o asincrona. Questo permette di estendere molto velocemente l’applicazione inserendo nuovi eventi che coprono nuove funzionalità. Ovviamente questo comporta che nuovi comandi siano creati e mandati all’applicazione.
Dalla versione 3.1 di Axon, questo bus permette di ricevere anche le ricerche sui dati, e di instradarle al corretto Query Handlers. In questo caso, ci si trova in una sorta di read-only mode dove le query e i DTO sono semplici oggetti non mutabili e sono utilizzabili direttamente da parte della UI.
Per realizzare l’Event Bus, Axon utilizza un modulo basato su AMQP, e garantisce che la consegna degli eventi sia fatta comunque anche in caso di temporanea mancanza di nodi che possono processarli.
Un po’ di configurazione
Per riuscire a vedere qualcosa di funzionante, andiamo ad aggiungere le configurazioni necessarie ad AxonDB per funzionare come database per il salvataggio degli eventi e dell’Event Bus. Il codice si occupa di configurare correttamente la parte di Axon per l’interrogazione dei dati, il datasource relativamente a PostgreSQL e AxonDb, e l’Event Bus.
In tutti questi passaggi, SpringBoot ci aiuta a ridurre in maniera sensibile tutta la configurazione dei bean grazie all’implementazione del paradigma “convention over configuration”. Parte della configurazione viene fatta attraverso le properties dell’applicazione, senza bisogno di scrivere molto codice o lunghi file XML.
EventStoreConfig
In questo file andiamo a configurare l’Event Bus. Avendo deciso di utilizzare AxonDB, il nostro bean restituirà un oggetto che implementa AxonDBEventStore come mostrato nel codice seguente:
In pratica andiamo a passare una configurazione, che nel nostro caso viene letta da file application.properties (indirizzo e porta a cui risponde AxonDB), con il tipo di serializzatore da utilizzare per i messaggi. Le altre configurazioni permettono di indicare a Hibernate/JPA dove fare lo scan delle Entity e di configurare il processore per gli Eventi.
Unica nota è la definizione della saga, che nel nostro esempio non usiamo. Ma è necessaria per evitare problemi a runtime. La nostra configurazione utilizza un’implementazione in-memory per evitare di dover definire tabelle nel nostro DB.
Nel gergo comune di DDD e CQRS una saga non è altro che uno speciale Event Listener che permette di utilizzare le transazioni. La particolarità è che una saga può durare da pochi millesimi di secondo a giorni.
Query Handling
Questa è la parte che si occupa di processare le richiesta di lettura dei dati. Tipicamente si occupa di leggere dati aggregati scritti dagli eventi scatenati dai command. Possiamo definire un numero arbitrario di @QueryHandler; la discriminante saranno i parametri che stabiliscono quale method sarà invocato.
Usando Spring e Axon ci basta “decorare” il metodo con le annotazioni e lasciare fare al framework come possiamo vedere nel codice che segue:
I metodi non fanno altro che eseguire una query utilizzando JPA/Hibernate sulla tabella che fa da aggregatore dei nostri dati e che riflette tutti gli aggiornamenti che gli eventi generano e che vanno a modificare i nostri dati.
Tutto finito? In verità manca ancora da definire in che modo sono salvati gli eventi che vengono processati all’interno del nostro DB, in maniera tale da poterli poi interrogare. Possiamo trovare la parte che ci interessa sempre dentro la classe CardSummaryProjection.java nei metodi annotati con @EventHandler come nel codice riportato di seguito:
In base al parametro, processeremo la creazione (IssuedEvt) o la redimazione (RedeemedEvt) andando a inserire un nuovo record di tipo CardSummary o andando ad aggiornare il valore dell’amount.
Finito di aggiornare il DB, notifichiamo al nostro query bus che c’è stato un cambiamento: questo farà si che la nostra User Interface venga “avvertita” del cambiamento e visualizzi l’aggiornamento.
UI
Il progetto è accompagnato da una User Interface minimale sviluppata utilizzando Vaadin [6]. Solo questo basterebbe a scrivere qualche articolo… magari in futuro. A noi basta sapere che con 2 semplici classi Java siamo in grado di gestire una UI minimale come quella in figura 1 che reagisce ai nostri eventi tramite websocket.
Gli handler si occupano di mandare comandi sul nostro bus mentre nella classe CardSummaryDataProvider.java ci agganciamo al bus creato per la parte query e stiamo in attesa delle risposte contenenti i dati. Il framework si occuperà di eseguire gli aggiornamenti della UI.
Conclusioni
In questo primo articolo abbiamo visto come creare una piccola applicazione utilizzando Spring e Axon per implementare in maniera veloce il pattern CQRS. Entrambi i framework aiutano parecchio permettendo di non perdere tempo dietro a configurazioni di event bus e a tutto il resto. Potete trovare tutto il codice funzionante con Java versione 8 nel mio repository GitHub sotto la branch mono.
Nel prossimo articolo dividerò la nostra applicazione monolitica creando dei “microservizi” in maniera tale da suddividere le varie parti Command, Query e UI in applicazioni separate e raccordando tutto con i vari EventBus.
Questo esercizio spero vi porti a pensare che, pur avendo un’applicazione monolitica, con l’architettura CQRS risulta semplice pensare di aggiungere ulteriori funzionalità senza rischiare di introdurre bug.