Event Sourcing con Laravel nel 2026: quando ha senso per una PMI e quando bastano alternative più semplici

Event Sourcing con Laravel nel 2026: quando ha senso per una PMI e quando bastano alternative più semplici

Lo scorso autunno ho gestito una decisione architetturale che riassume il motivo per cui questo articolo esiste. Una PMI lombarda del settore biomedicale doveva aggiungere "tracciabilità completa delle modifiche" a un'applicazione Laravel di gestione dispositivi medici, requisito imposto da un audit interno di certificazione ISO 13485 e dalla Direttiva NIS2 che li riguarda direttamente come operatore sanitario. Il responsabile IT mi ha contattato dicendo "abbiamo letto di Event Sourcing, vogliamo implementarlo". Dopo un'ora di discussione, abbiamo concluso che Event Sourcing non era la risposta giusta per il loro caso. Era un'ottima risposta a una domanda diversa. La risposta giusta - un audit log immutabile basato su hash chain SHA-256 - è arrivata in 4 giorni di lavoro. La risposta sbagliata - full Event Sourcing con aggregate root, projector e command bus su 60 entità di dominio esistenti - sarebbe stata un progetto da 4 mesi e probabilmente un fallimento.

Questo articolo non è "come implementare Event Sourcing in Laravel". È prima di tutto "quando ha senso per una PMI" e poi, se la risposta è sì, "come farlo bene con gli strumenti del 2026".

Cosa risolve davvero Event Sourcing (e cosa NON risolve)

Event Sourcing è un pattern architetturale che inverte la prospettiva sullo stato. Nel modello CRUD tradizionale, lo stato dell'applicazione è la verità: una riga in orders rappresenta l'ordine come è ora. Le modifiche sono sovrascritture, e ogni UPDATE cancella l'informazione precedente. Nel modello Event Sourced, lo stato è una proiezione: la verità è la sequenza immutabile di eventi (OrderCreated, OrderItemAdded, OrderShipped, OrderCancelled) e lo stato corrente si ricostruisce replicandoli. Il database non memorizza più "l'ordine adesso", memorizza "tutto quello che è successo all'ordine". Per una formalizzazione classica del concetto, Martin Fowler ha pubblicato il pattern Event Sourcing nel 2005 e la sua descrizione resta tutt'oggi il riferimento migliore.

Questa inversione non è cosmetica. Cambia tre cose concrete:

  1. Hai un audit log nativo per costruzione. Non c'è bisogno di un audit_log separato - la sequenza di eventi È l'audit log. Ogni stato passato è ricostruibile per replay.
  2. Puoi rispondere a domande temporali. "Qual era il prezzo di questo prodotto il 15 gennaio 2026 alle 14:32?" è una query di replay, non una struttura dati che devi aver progettato in anticipo.
  3. Disaccoppi le scritture dalle letture. Gli eventi sono il modello di scrittura, le proiezioni (tabelle ottimizzate per la query) sono il modello di lettura. Questo è il punto di contatto naturale con CQRS (Command Query Responsibility Segregation).

Quello che Event Sourcing non risolve, e che molti articoli sbrigativi vendono come benefici: non aumenta automaticamente la performance (le proiezioni vanno mantenute, e replay di eventi su milioni di record è lento), non sostituisce il bisogno di backup, non è una garanzia di consistenza distribuita, e - questo è il punto più sottovalutato - non è amichevole con il GDPR. Il diritto all'oblio dell'art. 17 GDPR pretende la cancellazione dei dati personali, ma il modello Event Sourced si basa sull'immutabilità della storia degli eventi. Su questo torno alla fine.

Event Sourcing risponde alla domanda "perché siamo arrivati allo stato attuale?". CRUD risponde alla domanda "qual è lo stato attuale?". Sono due domande diverse, e moltissime applicazioni PMI hanno solo bisogno della seconda.

Quando ha davvero senso per una PMI nel 2026?

In dieci anni di consulenza ho visto decine di "audit trail" implementati con success e qualche tentativo fallito di Event Sourcing. La regola operativa che applico è semplice: Event Sourcing ha senso quando il "perché" del cambiamento è una domanda business ricorrente, non quando l'audit è solo un requisito di compliance.

In concreto, lo consiglio nei seguenti casi:

  • Sistemi finanziari/contabili dove ogni movimento ha valore legale. Conti correnti, libri mastri, sistemi di fatturazione elettronica con SDI dove la sequenza degli eventi È il modello di business. La banca centrale del mio cliente più "esoterico" gira proprio su questo pattern.
  • Workflow complessi dove la storia delle decisioni è oggetto di analisi. Approvazioni multi-step, gestione richieste di rimborso, ticketing con escalation. Il "perché" l'utente è arrivato a uno stato è un dato di business.
  • Sistemi che devono ricostruire stati passati per analisi. Dispositivi IoT con telemetria storica, sistemi di trading con backtesting, applicazioni che devono rispondere a "cosa avremmo visto se avessimo guardato quel giorno?".

Lo sconsiglio quando:

  • L'audit è un requisito di compliance ma il business non ne usa mai la storia. In questo caso un audit log immutabile semplice (vedi sezione successiva) costa il 5% dell'effort di Event Sourcing e copre lo stesso requisito.
  • L'applicazione è già un monolite CRUD maturo con decine di entità. Refactorare un'app Laravel di 5 anni a Event Sourcing significa riscrivere il modello di dominio, e nessun cliente PMI ha il budget per un'operazione del genere senza un payoff business chiaro. Un percorso più pragmatico è il refactoring incrementale verso Service Layer e Repository Pattern, che ho documentato come passaggio intermedio per i Fat Controller Laravel.
  • Il team non ha esperienza precedente con DDD o Event-Driven design. Event Sourcing impone una curva di apprendimento ripida, e gli errori architetturali sui primi mesi compromettono l'intero progetto.

Per il caso del cliente biomedicale che raccontavo all'inizio, nessuno dei tre scenari positivi era applicabile: avevano bisogno di "chi ha modificato cosa, quando, e poter dimostrare che l'audit log non è stato manomesso". È un requisito di compliance, non di business. Vediamo cosa abbiamo fatto.

L'alternativa pragmatica: audit-chain immutabile con hash SHA-256

Per il 70-80% dei requisiti di tracciabilità di una PMI, la soluzione corretta non è Event Sourcing ma un audit log immutabile con catena di hash crittografici. Il pattern è semplice: ogni record di audit memorizza un hash SHA-256 calcolato sui dati del record stesso più l'hash del record precedente. Se qualcuno tenta di modificare un record passato, la catena di hash si rompe e la manomissione è rilevabile per verifica matematica.

Su Laravel, il pacchetto graymatter/laravel-audit-chain implementa esattamente questo pattern, ed è progettato dichiaratamente per GDPR articoli 15, 17, 33 e NIS2 articolo 21. Offre due modalità: HasActivityLog (audit log "leggero" senza catena crittografica) e HasAuditTrail (modalità full con SHA-256 hash chain). Il codice di applicazione su un model è banale:

// app/Models/Device.php
use GrayMatter\LaravelAuditChain\Concerns\HasAuditTrail;
use GrayMatter\LaravelAuditChain\Attributes\PersonalData;

class Device extends Model
{
    use HasAuditTrail;

    #[PersonalData]
    protected $fillable = [
        'serial_number',
        'patient_id', // dato personale → annotato
        'last_calibration_at',
        'firmware_version',
    ];
}

L'attributo #[PersonalData] segnala al package quali campi sono dati personali, abilitando export/anonymization conforme all'art. 15 GDPR (diritto di accesso) e all'art. 17 (diritto all'oblio) tramite redazione mirata. Gli Eloquent guard del package impediscono update e delete sui record di audit a livello applicativo, e la tabella audit_trail può essere protetta a livello DB con un GRANT che concede solo INSERT all'utente applicativo, mai UPDATE o DELETE (questa è la barriera reale). Per il cliente biomedicale, questa configurazione ha soddisfatto sia l'auditor ISO 13485 sia il DPO interno per NIS2.

Quando questo NON basta, e ti serve davvero la potenza di Event Sourcing, si passa al pattern grosso.

Spatie laravel-event-sourcing v7 con CQRS in produzione

Se hai mappato il tuo dominio e hai concluso che Event Sourcing è la risposta giusta, Spatie laravel-event-sourcing v7 è oggi il pacchetto di riferimento per Laravel. Maintainership solida, documentazione estesa, integrato con Laravel Horizon per la coda di processing, e - feature centrale di v7 - supporto nativo a CQRS via command bus con mapping basato su PHP attribute. La novità rispetto alle versioni precedenti è che non devi più scrivere command handler manuali per i casi semplici: dichiari un'attribute #[HandledBy] sulla classe Command e l'aggregate root riceve il command direttamente.

Esempio di un command per aggiungere un item al carrello, mappato automaticamente su CartAggregateRoot:

// app/Domain/Cart/Commands/AddCartItem.php
use App\Domain\Cart\CartAggregateRoot;
use Spatie\EventSourcing\Commands\Attributes\AggregateUuid;
use Spatie\EventSourcing\Commands\Attributes\HandledBy;

#[HandledBy(CartAggregateRoot::class)]
class AddCartItem
{
    public function __construct(
        #[AggregateUuid] public string $cartUuid,
        public string $cartItemUuid,
        public string $sku,
        public int $quantity,
    ) {}
}

E l'aggregate root riceve il command direttamente come metodo, senza handler intermediari:

// app/Domain/Cart/CartAggregateRoot.php
use Spatie\EventSourcing\AggregateRoots\AggregateRoot;

class CartAggregateRoot extends AggregateRoot
{
    private array $items = [];

    public function addCartItem(AddCartItem $command): void
    {
        if (count($this->items) >= 50) {
            throw new \DomainException('Limite massimo di 50 articoli per carrello');
        }

        $this->recordThat(new CartItemAdded(
            cartItemUuid: $command->cartItemUuid,
            sku: $command->sku,
            quantity: $command->quantity,
        ));
    }

    protected function applyCartItemAdded(CartItemAdded $event): void
    {
        $this->items[$event->cartItemUuid] = [
            'sku' => $event->sku,
            'quantity' => $event->quantity,
        ];
    }
}

Da un controller, dispatchi un command sul CommandBus di Spatie:

// app/Http/Controllers/Api/CartController.php
use Spatie\EventSourcing\Commands\CommandBus;

public function addItem(Request $request, CommandBus $bus, string $cartUuid): JsonResponse
{
    $bus->dispatch(new AddCartItem(
        cartUuid: $cartUuid,
        cartItemUuid: (string) Str::uuid(),
        sku: $request->validated('sku'),
        quantity: $request->validated('quantity'),
    ));

    return response()->json(['status' => 'queued'], 202);
}

Tre dettagli operativi che ti salvano in produzione. Primo, abilita la cache dei projector e reactor scoperti in config/event-sourcing.php (opzione cache_path), altrimenti su ogni request Laravel re-scansiona la directory. Secondo, dedica una coda specifica a un singolo worker (processes => 1): gli eventi devono essere processati in ordine, ed è esattamente la garanzia che vuoi su un sistema event-sourced. Su Horizon configura un supervisor dedicato event-sourcing-supervisor-1 con balance: simple. Terzo, non usare lo stesso DB della web application come event store su carichi alti: dopo qualche milione di eventi le query di replay diventano lente, e isolare l'event store su un DB dedicato (anche solo logicamente) ti permette di gestire il sizing in modo indipendente.

Una nota di disambiguazione importante per chi viene dal mondo CRUD: Event Sourcing non è la stessa cosa degli "eventi Laravel" che ascolti con Event::listen() o con l'attributo #[AsEventListener]. Quelli sono notifiche asincrone interne all'applicazione, mentre gli eventi di un sistema Event Sourced sono fatti di dominio persistiti come source of truth. Se stai modernizzando il sistema di eventi ordinario di Laravel - ad esempio passando dall'array $listen all'event discovery - quel refactoring rientra in un altro tipo di intervento, che ho coperto nell'articolo su come modernizzare la gestione eventi Laravel con #[AsEventListener] in L12. Sono due livelli architetturali diversi e confonderli porta a errori di scope.

Il tranello GDPR: immutabilità degli eventi vs diritto all'oblio

Questo è il punto che gli articoli sbrigativi su Event Sourcing non affrontano, e che invece va affrontato per primo se la tua applicazione tocca dati personali. Il GDPR all'art. 17 garantisce all'interessato il diritto alla cancellazione dei propri dati. Un event store immutabile, per definizione, non cancella nulla. C'è un conflitto diretto.

Le due strategie operative che ho applicato:

  • Crypto-shredding. Ogni evento che contiene dati personali viene cifrato con una chiave dedicata per soggetto interessato. Quando arriva una richiesta di cancellazione GDPR, non cancelli gli eventi: cancelli la chiave. Gli eventi diventano matematicamente illeggibili, soddisfacendo lo scopo dell'art. 17 ("rendere i dati non più disponibili") senza rompere l'immutabilità del log. È l'approccio raccomandato da HashiCorp con Vault per event sourcing GDPR-compliant, e l'ho implementato su Laravel usando Vault Transit come provider di chiavi.
  • Eventi "dummy" di censura. Un evento speciale PersonalDataRedacted che, replayato, sostituisce tutti i campi PII di una serie di eventi precedenti. Più semplice del crypto-shredding ma non garantisce che i dati originali non siano rimasti in backup o snapshot. Da preferire solo se hai un piano di rotazione dei backup compatibile.

In entrambi i casi serve coordinamento con il DPO e con la documentazione del registro dei trattamenti. Per i pattern operativi di gestione GDPR su Laravel ho coperto i casi più comuni nella checklist d'emergenza GDPR per Laravel, e per il quadro NIS2 più ampio ho scritto la mia guida pratica alla compliance NIS2 per server Hetzner/OVH.

Nessuna delle due strategie è gratis: il crypto-shredding richiede un sistema di gestione chiavi solido (KMS o Vault, non file su disco), gli eventi di censura richiedono che il replay dell'event store gestisca correttamente l'ordine logico. Sono tecnicamente fattibili ma sono l'opposto del "plug-and-play". Questa è una delle ragioni per cui per la maggior parte delle PMI consiglio la route audit-chain immutabile con hash: il GDPR si gestisce con DELETE mirate sui campi PII di una tabella di audit normale, e la garanzia di non manomissione viene dalla catena di hash, non dall'immutabilità del database.

Event Sourcing è uno degli strumenti più potenti del bagaglio di un architetto Laravel moderno, ma è anche uno dei più costosi da adottare male. La domanda giusta da farsi non è "come implemento Event Sourcing", è "quale problema sto risolvendo, e qual è la soluzione minima sufficiente". Per il cliente biomedicale di cui ti raccontavo, la minima sufficiente è stata audit-chain con hash; per un altro cliente del settore fintech che aveva bisogno di ricostruire saldi storici per audit BCE, la minima sufficiente è stata Spatie event-sourcing con CQRS sul dominio specifico dei movimenti finanziari, lasciando il resto dell'applicazione come CRUD normale. La differenza la fa la diagnosi prima dell'implementazione. Se stai valutando un'iniziativa di tracciabilità o audit per la tua applicazione Laravel e vuoi capire qual è la strada giusta per il tuo caso senza buttare mesi di sviluppo nella direzione sbagliata, scopri il mio approccio professionale ai temi di architettura Laravel - lavoro con Laravel da Laravel 4 e ho visto entrambi gli scenari (Event Sourcing che funziona benissimo, e Event Sourcing che diventa una palla al piede). Se vuoi una valutazione concreta del tuo caso specifico, contattami per una consulenza: in due settimane di audit ti consegno una raccomandazione architetturale supportata da numeri (effort, rischio, payoff) e una decisione che puoi portare al CdA con onestà tecnica.

Ultima modifica: