Salta al contenuto principale

3 - CQRS e Event Sourcing con TypeScript

CQRS e Event Sourcing con TypeScript CQRS (Command Query Responsibility Segregation) e Event Sourcing sono due pattern architetturali che, combinati,...

Cosa ne pensi di questo articolo?

3 - CQRS e Event Sourcing con TypeScript
Architettura SoftwareFoto:Ruben BoekelooPexels

In Breve

CQRS e Event Sourcing con TypeScript CQRS (Command Query Responsibility Segregation) e Event Sourcing sono due pattern architetturali che, combinati,...

CQRS e Event Sourcing con TypeScript

CQRS (Command Query Responsibility Segregation) e Event Sourcing sono due pattern architetturali che, combinati, offrono un modo radicalmente diverso di progettare sistemi software. Invece di leggere e scrivere sullo stesso modello dati, CQRS separa le operazioni di scrittura (comandi) da quelle di lettura (query). Event Sourcing, a sua volta, sostituisce la persistenza tradizionale dello stato con una sequenza ordinata di eventi immutabili che descrivono tutto ciò che è accaduto nel sistema.

In questo articolo esploreremo entrambi i pattern in profondità, con implementazioni concrete in TypeScript. Useremo un sistema di gestione ordini come esempio per illustrare comandi, query, event store, proiezioni e snapshot.

Cosa Imparerai

  • Il pattern CQRS: separazione tra comandi e query
  • Command Bus e Query Bus in TypeScript
  • Event Sourcing: persistenza basata su eventi
  • Implementazione di un Event Store
  • Proiezioni e Read Models
  • Snapshot per ottimizzare le performance
  • Quando usare (e quando evitare) CQRS e Event Sourcing

CQRS: Separazione tra Comandi e Query

Il principio alla base di CQRS è semplice: il modello usato per scrivere dati non deve essere lo stesso usato per leggerli. In un'architettura tradizionale, un unico modello (e spesso un'unica tabella) gestisce sia le operazioni di scrittura sia quelle di lettura. CQRS introduce due modelli distinti, ciascuno ottimizzato per il proprio scopo.

Comandi

Un Command rappresenta l'intenzione di modificare lo stato del sistema. È un oggetto immutabile che contiene tutti i dati necessari per eseguire l'operazione. I comandi non restituiscono dati: il loro unico effetto è la modifica dello stato.

Definizione di Command e Command Handler

// cqrs/command.ts

export interface Command {
  readonly type: string;
}

export interface CommandHandler<T extends Command> {
  execute(command: T): Promise<void>;
}

// commands/create-order.command.ts

export class CreateOrderCommand implements Command {
  readonly type = 'CreateOrder';

  constructor(
    public readonly orderId: string,
    public readonly customerId: string,
    public readonly items: {
      productId: string;
      productName: string;
      quantity: number;
      unitPrice: number;
    }[]
  ) {}
}

// commands/handlers/create-order.handler.ts

export class CreateOrderHandler
  implements CommandHandler<CreateOrderCommand> {

  constructor(private readonly eventStore: EventStore) {}

  async execute(command: CreateOrderCommand): Promise<void> {
    // Verifica che l'ordine non esista gia
    const existing = await this.eventStore.getEvents(command.orderId);
    if (existing.length > 0) {
      throw new Error('Ordine già esistente');
    }

    // Crea l'aggregate e genera gli eventi
    const order = Order.create(
      command.orderId,
      command.customerId,
      command.items
    );

    // Salva gli eventi generati
    const events = order.pullUncommittedEvents();
    await this.eventStore.appendEvents(command.orderId, events, 0);
  }
}

Query

Una Query rappresenta una richiesta di dati. Non modifica lo stato del sistema e restituisce un risultato ottimizzato per il caso d'uso specifico. Il modello di lettura (Read Model) può essere denormalizzato, precompilato e strutturato esattamente come serve alla UI.

Definizione di Query e Query Handler

// cqrs/query.ts

export interface Query {
  readonly type: string;
}

export interface QueryHandler<TQuery extends Query, TResult> {
  execute(query: TQuery): Promise<TResult>;
}

// queries/get-order-summary.query.ts

export class GetOrderSummaryQuery implements Query {
  readonly type = 'GetOrderSummary';

  constructor(public readonly orderId: string) {}
}

export interface OrderSummaryReadModel {
  orderId: string;
  customerName: string;
  status: string;
  itemCount: number;
  totalAmount: number;
  createdAt: string;
  lastUpdatedAt: string;
}

// queries/handlers/get-order-summary.handler.ts

export class GetOrderSummaryHandler
  implements QueryHandler<GetOrderSummaryQuery, OrderSummaryReadModel> {

  constructor(private readonly readDb: ReadDatabase) {}

  async execute(
    query: GetOrderSummaryQuery
  ): Promise<OrderSummaryReadModel> {
    const summary = await this.readDb.findOne<OrderSummaryReadModel>(
      'order_summaries',
      { orderId: query.orderId }
    );

    if (!summary) {
      throw new Error('Ordine non trovato');
    }

    return summary;
  }
}

Command Bus e Query Bus

Il Bus è il meccanismo che collega comandi/query ai rispettivi handler. Funge da dispatcher centralizzato, disaccoppiando chi invia il comando da chi lo gestisce.

Command Bus e Query Bus

// cqrs/command-bus.ts

export class CommandBus {
  private handlers = new Map<string, CommandHandler<any>>();

  register<T extends Command>(
    commandType: string,
    handler: CommandHandler<T>
  ): void {
    if (this.handlers.has(commandType)) {
      throw new Error(`Handler già registrato per: ${commandType}`);
    }
    this.handlers.set(commandType, handler);
  }

  async dispatch(command: Command): Promise<void> {
    const handler = this.handlers.get(command.type);
    if (!handler) {
      throw new Error(`Nessun handler per il comando: ${command.type}`);
    }
    await handler.execute(command);
  }
}

// cqrs/query-bus.ts

export class QueryBus {
  private handlers = new Map<string, QueryHandler<any, any>>();

  register<TQuery extends Query, TResult>(
    queryType: string,
    handler: QueryHandler<TQuery, TResult>
  ): void {
    this.handlers.set(queryType, handler);
  }

  async dispatch<TResult>(query: Query): Promise<TResult> {
    const handler = this.handlers.get(query.type);
    if (!handler) {
      throw new Error(`Nessun handler per la query: ${query.type}`);
    }
    return handler.execute(query);
  }
}

Event Sourcing: Lo Stato come Sequenza di Eventi

L'Event Sourcing capovolge il modo tradizionale di pensare alla persistenza. Invece di salvare lo stato corrente di un'entità, salviamo ogni evento che ha portato a quello stato. Lo stato attuale si ottiene riproducendo (replay) tutti gli eventi in ordine cronologico.

Vantaggi dell'Event Sourcing

Vantaggio Descrizione
Audit trail completo Ogni cambiamento è registrato; si può ricostruire lo stato a qualsiasi punto nel tempo
Debugging avanzato Riproducendo gli eventi si può capire esattamente come si è arrivati allo stato attuale
Proiezioni multiple Dagli stessi eventi si possono costruire read model diversi per esigenze diverse
Evoluzione temporale Nuove proiezioni possono essere create retroattivamente, riprocessando gli eventi storici
Nessuna perdita di dati Gli eventi sono immutabili e append-only; non si cancella mai nulla

Domain Events per Event Sourcing

Eventi di Dominio - Sistema Ordini

// events/base-event.ts

export interface DomainEvent {
  readonly eventType: string;
  readonly aggregateId: string;
  readonly occurredOn: Date;
  readonly version: number;
  readonly payload: Record<string, unknown>;
}

// events/order-events.ts

export class OrderCreatedEvent implements DomainEvent {
  readonly eventType = 'OrderCreated';
  readonly occurredOn = new Date();

  constructor(
    public readonly aggregateId: string,
    public readonly version: number,
    public readonly payload: {
      customerId: string;
      items: {
        productId: string;
        productName: string;
        quantity: number;
        unitPrice: number;
      }[];
    }
  ) {}
}

export class OrderConfirmedEvent implements DomainEvent {
  readonly eventType = 'OrderConfirmed';
  readonly occurredOn = new Date();

  constructor(
    public readonly aggregateId: string,
    public readonly version: number,
    public readonly payload: {
      confirmedAt: string;
      totalAmount: number;
    }
  ) {}
}

export class OrderItemAddedEvent implements DomainEvent {
  readonly eventType = 'OrderItemAdded';
  readonly occurredOn = new Date();

  constructor(
    public readonly aggregateId: string,
    public readonly version: number,
    public readonly payload: {
      productId: string;
      productName: string;
      quantity: number;
      unitPrice: number;
    }
  ) {}
}

export class OrderCancelledEvent implements DomainEvent {
  readonly eventType = 'OrderCancelled';
  readonly occurredOn = new Date();

  constructor(
    public readonly aggregateId: string,
    public readonly version: number,
    public readonly payload: {
      reason: string;
      cancelledAt: string;
    }
  ) {}
}

Aggregate con Event Sourcing

In un sistema Event Sourced, l'aggregate non mantiene lo stato direttamente ma lo ricostruisce applicando gli eventi. Ogni operazione di business genera nuovi eventi che vengono accumulati e poi persistiti nell'Event Store.

Event-Sourced Aggregate - Order

// aggregates/order.aggregate.ts

export class Order {
  private _id: string = '';
  private _customerId: string = '';
  private _status: OrderStatus = OrderStatus.CREATED;
  private _items: OrderItem[] = [];
  private _version: number = 0;
  private _uncommittedEvents: DomainEvent[] = [];

  // Factory method: crea un nuovo ordine
  static create(
    orderId: string,
    customerId: string,
    items: { productId: string; productName: string;
             quantity: number; unitPrice: number }[]
  ): Order {
    const order = new Order();
    order.apply(new OrderCreatedEvent(orderId, 1, {
      customerId,
      items,
    }));
    return order;
  }

  // Ricostruzione da eventi storici
  static fromHistory(events: DomainEvent[]): Order {
    const order = new Order();
    for (const event of events) {
      order.applyFromHistory(event);
    }
    return order;
  }

  confirm(): void {
    if (this._status !== OrderStatus.CREATED) {
      throw new Error('Solo ordini in stato CREATED possono essere confermati');
    }
    if (this._items.length === 0) {
      throw new Error('Impossibile confermare un ordine vuoto');
    }

    const total = this._items.reduce(
      (sum, item) => sum + item.quantity * item.unitPrice, 0
    );

    this.apply(new OrderConfirmedEvent(this._id, this._version + 1, {
      confirmedAt: new Date().toISOString(),
      totalAmount: total,
    }));
  }

  cancel(reason: string): void {
    if (this._status === OrderStatus.SHIPPED) {
      throw new Error('Impossibile cancellare un ordine già spedito');
    }

    this.apply(new OrderCancelledEvent(this._id, this._version + 1, {
      reason,
      cancelledAt: new Date().toISOString(),
    }));
  }

  // Applica un nuovo evento (genera uncommitted event)
  private apply(event: DomainEvent): void {
    this.mutate(event);
    this._uncommittedEvents.push(event);
  }

  // Applica un evento storico (senza generare uncommitted)
  private applyFromHistory(event: DomainEvent): void {
    this.mutate(event);
  }

  // Modifica lo stato interno in base all'evento
  private mutate(event: DomainEvent): void {
    switch (event.eventType) {
      case 'OrderCreated':
        this._id = event.aggregateId;
        this._customerId = (event.payload as any).customerId;
        this._items = (event.payload as any).items.map((i: any) => ({
          productId: i.productId,
          productName: i.productName,
          quantity: i.quantity,
          unitPrice: i.unitPrice,
        }));
        this._status = OrderStatus.CREATED;
        break;

      case 'OrderConfirmed':
        this._status = OrderStatus.CONFIRMED;
        break;

      case 'OrderItemAdded':
        this._items.push(event.payload as any);
        break;

      case 'OrderCancelled':
        this._status = OrderStatus.CANCELLED;
        break;
    }
    this._version = event.version;
  }

  pullUncommittedEvents(): DomainEvent[] {
    const events = [...this._uncommittedEvents];
    this._uncommittedEvents = [];
    return events;
  }

  get id(): string { return this._id; }
  get version(): number { return this._version; }
  get status(): OrderStatus { return this._status; }
}

Event Store

L'Event Store è il database degli eventi. Ogni stream di eventi corrisponde a un aggregate. Le operazioni fondamentali sono due: appendere nuovi eventi e leggere gli eventi di uno stream. L'Event Store garantisce l'ordinamento e supporta l'optimistic concurrency tramite il versioning.

Event Store - Implementazione In-Memory

// infrastructure/event-store.ts

export interface EventStore {
  appendEvents(
    streamId: string,
    events: DomainEvent[],
    expectedVersion: number
  ): Promise<void>;

  getEvents(streamId: string): Promise<DomainEvent[]>;

  getEventsFromVersion(
    streamId: string,
    fromVersion: number
  ): Promise<DomainEvent[]>;

  getAllEvents(): Promise<DomainEvent[]>;
}

export class InMemoryEventStore implements EventStore {
  private streams = new Map<string, DomainEvent[]>();
  private allEvents: DomainEvent[] = [];
  private subscribers: ((event: DomainEvent) => void)[] = [];

  async appendEvents(
    streamId: string,
    events: DomainEvent[],
    expectedVersion: number
  ): Promise<void> {
    const currentEvents = this.streams.get(streamId) || [];
    const currentVersion = currentEvents.length > 0
      ? currentEvents[currentEvents.length - 1].version
      : 0;

    // Optimistic concurrency check
    if (currentVersion !== expectedVersion) {
      throw new ConcurrencyError(
        `Conflitto di concorrenza: versione attesa ${expectedVersion}, ` +
        `versione corrente ${currentVersion}`
      );
    }

    const updatedStream = [...currentEvents, ...events];
    this.streams.set(streamId, updatedStream);
    this.allEvents.push(...events);

    // Notifica i subscriber (per le proiezioni)
    for (const event of events) {
      this.subscribers.forEach(sub => sub(event));
    }
  }

  async getEvents(streamId: string): Promise<DomainEvent[]> {
    return this.streams.get(streamId) || [];
  }

  async getEventsFromVersion(
    streamId: string,
    fromVersion: number
  ): Promise<DomainEvent[]> {
    const events = this.streams.get(streamId) || [];
    return events.filter(e => e.version > fromVersion);
  }

  async getAllEvents(): Promise<DomainEvent[]> {
    return [...this.allEvents];
  }

  subscribe(handler: (event: DomainEvent) => void): void {
    this.subscribers.push(handler);
  }
}

Proiezioni e Read Models

Le proiezioni sono il meccanismo che trasforma gli eventi in Read Models ottimizzati per la lettura. Ogni proiezione ascolta specifici tipi di eventi e aggiorna un modello di lettura denormalizzato. Questo modello è pensato per rispondere alle query nel modo più efficiente possibile.

Proiezione - Order Summary

// projections/order-summary.projection.ts

export interface OrderSummaryReadModel {
  orderId: string;
  customerId: string;
  status: string;
  itemCount: number;
  totalAmount: number;
  createdAt: string;
  updatedAt: string;
}

export class OrderSummaryProjection {
  private summaries = new Map<string, OrderSummaryReadModel>();

  constructor(eventStore: EventStore) {
    // Sottoscrizione agli eventi rilevanti
    eventStore.subscribe((event) => this.handleEvent(event));
  }

  private handleEvent(event: DomainEvent): void {
    switch (event.eventType) {
      case 'OrderCreated':
        this.onOrderCreated(event);
        break;
      case 'OrderConfirmed':
        this.onOrderConfirmed(event);
        break;
      case 'OrderItemAdded':
        this.onOrderItemAdded(event);
        break;
      case 'OrderCancelled':
        this.onOrderCancelled(event);
        break;
    }
  }

  private onOrderCreated(event: DomainEvent): void {
    const payload = event.payload as any;
    const items = payload.items || [];
    const totalAmount = items.reduce(
      (sum: number, i: any) => sum + i.quantity * i.unitPrice, 0
    );

    this.summaries.set(event.aggregateId, {
      orderId: event.aggregateId,
      customerId: payload.customerId,
      status: 'created',
      itemCount: items.length,
      totalAmount,
      createdAt: event.occurredOn.toISOString(),
      updatedAt: event.occurredOn.toISOString(),
    });
  }

  private onOrderConfirmed(event: DomainEvent): void {
    const summary = this.summaries.get(event.aggregateId);
    if (summary) {
      summary.status = 'confirmed';
      summary.totalAmount = (event.payload as any).totalAmount;
      summary.updatedAt = event.occurredOn.toISOString();
    }
  }

  private onOrderItemAdded(event: DomainEvent): void {
    const summary = this.summaries.get(event.aggregateId);
    if (summary) {
      const payload = event.payload as any;
      summary.itemCount += 1;
      summary.totalAmount += payload.quantity * payload.unitPrice;
      summary.updatedAt = event.occurredOn.toISOString();
    }
  }

  private onOrderCancelled(event: DomainEvent): void {
    const summary = this.summaries.get(event.aggregateId);
    if (summary) {
      summary.status = 'cancelled';
      summary.updatedAt = event.occurredOn.toISOString();
    }
  }

  // Query methods
  getById(orderId: string): OrderSummaryReadModel | undefined {
    return this.summaries.get(orderId);
  }

  getByCustomer(customerId: string): OrderSummaryReadModel[] {
    return Array.from(this.summaries.values())
      .filter(s => s.customerId === customerId);
  }

  getByStatus(status: string): OrderSummaryReadModel[] {
    return Array.from(this.summaries.values())
      .filter(s => s.status === status);
  }
}

Snapshot: Ottimizzare le Performance

Con il passare del tempo, il numero di eventi per un aggregate può crescere significativamente. Ricostruire lo stato da migliaia di eventi diventa costoso. Gli snapshot risolvono questo problema salvando periodicamente una fotografia dello stato dell'aggregate. La ricostruzione parte dallo snapshot più recente e applica solo gli eventi successivi.

Snapshot Store e Ricostruzione Ottimizzata

// infrastructure/snapshot-store.ts

export interface Snapshot {
  aggregateId: string;
  version: number;
  state: Record<string, unknown>;
  createdAt: Date;
}

export interface SnapshotStore {
  save(snapshot: Snapshot): Promise<void>;
  getLatest(aggregateId: string): Promise<Snapshot | null>;
}

export class InMemorySnapshotStore implements SnapshotStore {
  private snapshots = new Map<string, Snapshot>();

  async save(snapshot: Snapshot): Promise<void> {
    this.snapshots.set(snapshot.aggregateId, snapshot);
  }

  async getLatest(aggregateId: string): Promise<Snapshot | null> {
    return this.snapshots.get(aggregateId) || null;
  }
}

// repository/order.repository.ts

export class EventSourcedOrderRepository {
  private static readonly SNAPSHOT_INTERVAL = 50;

  constructor(
    private readonly eventStore: EventStore,
    private readonly snapshotStore: SnapshotStore
  ) {}

  async getById(orderId: string): Promise<Order> {
    // 1. Cerca lo snapshot più recente
    const snapshot = await this.snapshotStore.getLatest(orderId);

    let events: DomainEvent[];
    let order: Order;

    if (snapshot) {
      // 2a. Ricostruisci dallo snapshot + eventi successivi
      order = Order.fromSnapshot(snapshot.state);
      events = await this.eventStore.getEventsFromVersion(
        orderId,
        snapshot.version
      );
    } else {
      // 2b. Ricostruisci da tutti gli eventi
      events = await this.eventStore.getEvents(orderId);
      order = Order.fromHistory(events);
      return order;
    }

    // 3. Applica gli eventi mancanti
    for (const event of events) {
      order.applyFromHistory(event);
    }

    return order;
  }

  async save(order: Order): Promise<void> {
    const uncommitted = order.pullUncommittedEvents();
    const expectedVersion = order.version - uncommitted.length;

    await this.eventStore.appendEvents(
      order.id,
      uncommitted,
      expectedVersion
    );

    // Crea snapshot se necessario
    if (order.version % EventSourcedOrderRepository.SNAPSHOT_INTERVAL === 0) {
      await this.snapshotStore.save({
        aggregateId: order.id,
        version: order.version,
        state: order.toSnapshot(),
        createdAt: new Date(),
      });
    }
  }
}

Flusso Completo: Dal Comando alla Query

Vediamo il flusso completo di un'operazione nel sistema CQRS + Event Sourcing, dalla ricezione del comando fino alla query sul read model aggiornato.

Flusso completo - Orchestrazione

// app/order-service.ts

export class OrderService {
  constructor(
    private readonly commandBus: CommandBus,
    private readonly queryBus: QueryBus
  ) {}

  // Lato scrittura: invia un comando
  async createOrder(
    customerId: string,
    items: { productId: string; productName: string;
             quantity: number; unitPrice: number }[]
  ): Promise<string> {
    const orderId = crypto.randomUUID();

    await this.commandBus.dispatch(
      new CreateOrderCommand(orderId, customerId, items)
    );

    return orderId;
  }

  async confirmOrder(orderId: string): Promise<void> {
    await this.commandBus.dispatch(
      new ConfirmOrderCommand(orderId)
    );
  }

  // Lato lettura: esegui una query
  async getOrderSummary(orderId: string): Promise<OrderSummaryReadModel> {
    return this.queryBus.dispatch<OrderSummaryReadModel>(
      new GetOrderSummaryQuery(orderId)
    );
  }

  async getCustomerOrders(
    customerId: string
  ): Promise<OrderSummaryReadModel[]> {
    return this.queryBus.dispatch<OrderSummaryReadModel[]>(
      new GetCustomerOrdersQuery(customerId)
    );
  }
}

// Configurazione iniziale
function bootstrap(): OrderService {
  // Event Store
  const eventStore = new InMemoryEventStore();
  const snapshotStore = new InMemorySnapshotStore();
  const orderRepo = new EventSourcedOrderRepository(eventStore, snapshotStore);

  // Proiezioni
  const orderSummaryProjection = new OrderSummaryProjection(eventStore);
  const readDb = new InMemoryReadDatabase(orderSummaryProjection);

  // Command Bus
  const commandBus = new CommandBus();
  commandBus.register('CreateOrder', new CreateOrderHandler(eventStore));
  commandBus.register('ConfirmOrder', new ConfirmOrderHandler(orderRepo));

  // Query Bus
  const queryBus = new QueryBus();
  queryBus.register('GetOrderSummary', new GetOrderSummaryHandler(readDb));
  queryBus.register('GetCustomerOrders', new GetCustomerOrdersHandler(readDb));

  return new OrderService(commandBus, queryBus);
}

CQRS senza Event Sourcing

E' importante sottolineare che CQRS e Event Sourcing sono pattern indipendenti. Si può adottare CQRS senza Event Sourcing, mantenendo un database tradizionale per la scrittura e creando read model separati tramite proiezioni sincrone o asincrone.

Approccio Complessità Vantaggi Limitazioni
CQRS semplice Media Read model ottimizzati, separazione chiara Nessun audit trail nativo, eventuale consistency
CQRS + Event Sourcing Alta Audit trail, ricostruzione stato, proiezioni multiple Complessità gestionale, curva di apprendimento
Architettura tradizionale Bassa Semplice, familiare, tooling maturo Read e write sullo stesso modello, meno flessibilità

Quando Usare CQRS e Event Sourcing

Scenari Ideali

  • Requisiti di audit trail: settori regolamentati (finanza, sanita, compliance)
  • Read e write asimmetrici: molte più letture che scritture (o viceversa)
  • Proiezioni multiple: dati che devono essere letti in formati molto diversi
  • Sistemi collaborativi: più utenti modificano gli stessi dati concorrentemente
  • Event-driven architecture: integrazione con sistemi che comunicano tramite eventi

Quando Evitare

  • Applicazioni CRUD semplici: la complessità aggiunta non giustifica i benefici
  • Team piccoli senza esperienza: la curva di apprendimento e significativa
  • Requisiti di strong consistency: CQRS introduce eventual consistency tra read e write
  • Prototipi e MVP: la velocità di sviluppo iniziale è più importante della scalabilità

Sfide e Considerazioni

Sfida Descrizione Strategia di Mitigazione
Eventual Consistency Il read model potrebbe non riflettere immediatamente l'ultima scrittura UI ottimistica, polling, notifiche in tempo reale
Evoluzione degli eventi La struttura degli eventi cambia nel tempo Versioning degli eventi, upcasting, schema registry
Ricostruzione proiezioni Riprocessare milioni di eventi può richiedere tempo Snapshot, processing parallelo, proiezioni incrementali
Complessità operativa Più componenti da monitorare e mantenere Observability, logging strutturato, health check

Conclusione

CQRS e Event Sourcing sono strumenti potenti che risolvono problemi specifici di scala, audit e flessibilità. CQRS separa i modelli di lettura e scrittura, consentendo di ottimizzare ciascuno indipendentemente. Event Sourcing trasforma la persistenza in un registro immutabile di fatti, aprendo possibilità come il time-travel debugging, le proiezioni retroattive e l'audit trail nativo.

Come ogni pattern avanzato, richiedono una valutazione attenta del contesto. Non sono la soluzione a ogni problema di architettura, ma nei scenari giusti - sistemi complessi, domini event-driven, requisiti di compliance - offrono vantaggi che le architetture tradizionali non possono eguagliare. La chiave è iniziare con CQRS in forma semplice e introdurre Event Sourcing solo dove il valore aggiunto è chiaro e misurabile.

Domande frequenti

Qual è la principale differenza tra un modello di lettura (Read Model) e un modello di scrittura (Write Model) in CQRS?

Il modello di lettura (Read Model) è ottimizzato per le query e può essere denormalizzato, precompilato e strutturato esattamente come serve alla UI, mentre il modello di scrittura (Write Model) rappresenta l'intenzione di modificare lo stato del sistema.

Come funziona l'Event Store in un sistema Event Sourced?

L'Event Store è il database degli eventi e ogni stream di eventi corrisponde a un aggregate. Le operazioni fondamentali sono due: appendere nuovi eventi e leggere gli eventi di uno stream.

Qual è lo scopo delle proiezioni in un sistema Event Sourced?

Lo scopo delle proiezioni è trasformare gli eventi in Read Models ottimizzati per la lettura, aggiornando un modello di lettura denormalizzato e pensato per rispondere alle query nel modo più efficiente possibile.

Perché è importante utilizzare snapshot in un sistema Event Sourced?

Gli snapshot sono importanti perché salvano periodicamente una fotografia dello stato dell'aggregate, permettendo di ricostruire lo stato da migliaia di eventi e risolvendo il problema della crescita significativa del numero di eventi.

Fonti e approfondimenti

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti