Tech

Dai bounded context al codice: riscrivere un'insurtech, modellare un MMORTS da zero

dddbounded-contextarchitetturaevent-storming

Due storie opposte, entrambe dentro una startup. In viteSicure, un’insurtech, il nemico era un mostro legacy: un backend che fatturava circa un milione di euro all’anno, nato attorno a una sola polizza caso morte. In quel momento però ne vendeva già due, perché era stata aggiunta la polizza infortuni: due prodotti con due flussi differenti e nessuna astrazione comune. Ed era scritto male: senza test e con un accoppiamento altissimo. In EFGC, una startup che sviluppa videogame MMORTS, il problema era l’opposto: non c’era niente da smontare, perché non c’era ancora niente. C’era una piattaforma da modellare da zero e un team che sapeva di dover crescere in fretta.

In entrambi i casi il punto di partenza è stato lo stesso: non il codice, ma il dominio. E in entrambi i casi la bussola sono stati i bounded context. Ciò che è cambiato, radicalmente, è la forma che hanno preso. Perché non esiste l’architettura giusta: esiste l’architettura giusta per quel dominio, quel team, quel momento.

Da dove vengono i bounded context

Un bounded context non esce da un diagramma: esce dal business. In entrambe le esperienze lo strumento principale è stato l’ Event Storming, un workshop per far emergere il flusso di lavoro reale dell’azienda con i suoi eventi significativi, le responsabilità e i confini naturali. Il codice racconta come si è evoluto il software, non come funziona il business: per questo non siamo partiti da lì.

Il punto che conta di più però è cosa non abbiamo fatto: non abbiamo provato a disegnare tutto in anticipo. Niente design upfront. E non esiste “l’Event Storming” al singolare: non se ne fa uno solo per modellare tutto il dominio, ma uno per ogni parte da esplorare, quando serve. Nessun risultato è mai stato trattato come un artefatto immutabile: in entrambe le esperienze il modello è stato fatto evolvere e i confini sono stati rivisti nel tempo, man mano che la comprensione del dominio cresceva o era necessario fare discovery approfondita di una parte di esso.

In EFGC abbiamo anche scoperto strada facendo che l’Event Storming non era lo strumento migliore per la discovery e la modellazione di alcune parti del dominio. Per quelle aree abbiamo usato altre tecniche di modellazione collaborativa e di design emergente, continuando a identificare e raffinare i confini mentre il sistema cresceva.

In viteSicure l’Event Storming iniziale lasciava vaga e inesplorata la parte finale del flusso, perché sarebbe stata affrontata più avanti, al momento opportuno. Nella prima implementazione abbiamo preferito non appesantire il modello con un bounded context in più che non avevamo ancora esplorato a livello di dominio. Quando il momento è arrivato, abbiamo esplorato quella parte di flusso con un Event Storming dedicato, introdotto un nuovo BC, rivisto i confini e spostato nel nuovo contesto del codice già scritto. Quel codice non era sbagliato: semplicemente non era più nel luogo adatto.

Il principio che tiene insieme tutto questo è l’allineamento. Business, modelli e codice devono sempre raccontare la stessa storia: l’Event Storming, la mappa dei bounded context e il codice sono tre facce dello stesso dominio. Le discrepanze sono inevitabili mentre la comprensione cresce, ma devono essere note, transitorie e riallineate appena possibile. È quando diventano invisibili o permanenti che nasce il prossimo mostro legacy.

Dal bounded context alla forma architetturale

In viteSicure i bounded context sono diventati i moduli di un monolite modulare. Il dominio era abbastanza piccolo da stare in un unico backend e un monolite ben diviso ci dava confini netti senza il costo operativo dei microservizi. Sul legacy invece non abbiamo fatto refactor per organizzarlo attorno ai BC: quei confini non esistevano e ricostruirli lì dentro non aveva senso. Quello che serviva era identificare i punti dove tagliare o innestare il passaggio al nuovo sistema, un BC alla volta, mentre il vecchio continuava a vendere. Lo stesso accoppiamento rendeva difficile perfino capire quale codice fosse morto, e cancellarlo era rischioso: in questo lavoro ci ha aiutato l’intelligenza artificiale.

In EFGC i bounded context sono diventati microservizi indipendenti. Ma il punto non è “microservizi”: è che ogni BC ha la propria architettura interna, scelta in base alle necessità di quel sotto-dominio e indipendente da quella degli altri. Tra i BC l’accoppiamento è stato tenuto volutamente al minimo e le interfacce tra contesti erano stabili e definite in modo formale. Con un team in crescita, quei confini erano anche confini di autonomia.

Stesso principio, due forme opposte. Non per ideologia: per adattamento al contesto.

I confini dentro i confini: DDD tattico

In entrambi i casi abbiamo modellato il codice intorno ad aggregati, entity e value object. Vale la pena chiarire i termini, perché qui c’è la differenza che conta. Il bounded context è il confine della logica di business: al suo interno valgono un linguaggio e delle logiche di business precise. L’ aggregate-root è invece il confine della consistenza e della contesa dei dati: è l’unità entro cui garantiamo le invarianti e gestiamo gli accessi concorrenti, l’aggregate-root deve avere consistenza forte.

In viteSicure il dominio è semplice e la contesa dei dati praticamente inesistente, quindi ogni BC aveva un unico aggregate-root. Questa scelta ha portato nel codice la stessa semplicità e solidità del dominio. L’implementazione usava CQRS (Command Query Responsibility Segregation) e salvava su database lo stato dell’aggregato.

In EFGC il quadro era più vario. Prendendo in prestito i termini del framework Cynefin, alcuni BC erano semplici, con logica di dominio quasi nulla, mentre quelli core erano complicati, con tante regole. Nei BC più ricchi i confini di consistenza e contesa non coincidevano con l’intero contesto, ma potevano essere più piccoli: per questo alcuni BC avevano più aggregate-root. Queste differenze hanno prodotto differenze enormi nel codice: da moduli RESTful isolati, con pochissima logica e uno stato salvato su database in modo quasi trasparente, a moduli complicati scritti con CQRS e Event Sourcing, con policy di dominio per gestire gli eventi di integrazione generati dagli altri BC.

È cambiato anche il ruolo dei servizi. In viteSicure abbiamo creato spesso domain service e application service per orchestrare i flussi interni a un BC e per dialogare con il mondo esterno: carrier, provider di pagamento, firma digitale. In EFGC erano rari, perché la maggior parte delle logiche viveva dentro aggregati ed entity.

L’ architettura esagonale è stata la scelta quasi ovunque. In viteSicure i carrier sono modellati come porte e ogni compagnia è un adapter: aggiungere un nuovo carrier significa integrare solo il pezzo che parla con il suo sistema. La rara eccezione erano i BC RESTful più semplici di EFGC, praticamente senza regole di dominio: lì i controller HTTP accedevano quasi direttamente alle risorse salvate su database, ma anche in quel caso l’accesso al DB è sempre rimasto disaccoppiato dal protocollo usato per la chiamata.

L’evoluzione dei bounded context core di EFGC

In viteSicure questa evoluzione non c’è mai stata: il dominio era semplice e le architetture interne dei moduli sono rimaste quelle iniziali. In EFGC è successo il contrario, e anche questa differenza fa parte del confronto.

Il feedback dal sistema reale ha avuto un ruolo decisivo nell’evoluzione dei BC più complicati di EFGC, quelli con tante regole di dominio e più aggregati, implementati con CQRS e Event Sourcing. La loro architettura interna è cambiata più volte, guidata da metriche e osservabilità. Gli obiettivi erano reggere il carico, migliorare le performance e rendere il sistema più resiliente ai malfunzionamenti, ma anche ridurre i costi e poter scalare i singoli componenti in modo più granulare. Gli utenti erano milioni e potevano essere connessi contemporaneamente, e il carico sui singoli microservizi era difficile da prevedere.

  • Prima implementazione: un microservizio per BC, che eseguiva i comandi, costruiva le projection (i read-model) ed eseguiva le query.
  • Fase intermedia 1: ogni BC diviso in tre microservizi distinti: uno per eseguire i comandi, uno per costruire le projection reagendo agli eventi di dominio, uno per la parte di query.
  • Fase intermedia 2: il servizio che esegue i comandi diviso a sua volta in un microservizio per ogni aggregate-root.
  • Implementazione finale: i microservizi di projection e query smantellati in piccole lambda function, ognuna con una singola responsabilità: aggiornare un dato a fronte di un evento interno, oppure eseguire una ricerca su una projection dedicata.

Tutto girava su AWS, sfruttando servizi nativi come Lambda e DynamoDB. In tutta questa evoluzione abbiamo cercato di evitare le saga, cioè le transazioni distribuite coordinate a eventi, e le abbiamo ridotte al minimo indispensabile. Un risultato possibile solo grazie a una divisione accurata in bounded context e aggregate-root, raffinata in modo continuo.

Il punto è che questi cambiamenti sono rimasti dentro i confini. Cambiare tre volte l’architettura interna di un bounded context non ha travolto gli altri: è esattamente il senso di avere dei confini.

La forma segue il contesto

Monolite modulare contro microservizi e lambda. Event Sourcing contro salvataggio dello stato su database. Nessuna di queste scelte era “la risposta giusta” in astratto: era la risposta giusta per quel dominio, quel team, quella fase di crescita. E quando il contesto è cambiato, come nei BC core di EFGC, la forma è cambiata con lui, guidata dal feedback del sistema in produzione e senza che il resto del sistema se ne accorgesse.

Se c’è un filo che tiene insieme queste due storie, è questo: si parte dal dominio, lo si esplora insieme al business e si tiene il codice allineato ai modelli man mano che la comprensione cresce. Il resto — monolite o microservizi, stato o eventi, servizi o lambda — è una conseguenza.

Approfondimenti

Le storie complete: viteSicure, la riscrittura senza fermare il business ed EFGC, la crescita del team e del modo di lavorare. Il lato strategico — come emergono i confini del dominio insieme al business — è raccontato in l’Event Storming come punto di partenza.

Il metodo dietro il codice

Le pratiche tecniche funzionano quando aiutano il team a lavorare con più autonomia.

Scopri il metodo