Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Preferred way of handling "Event Sourcing" in NestJS CQRS recipe

I have been trying to figure out the preferred way of doing "Event Sourcing" while using the NestJS CQRS recipe (https://docs.nestjs.com/recipes/cqrs).

I've been looking at the NestJS framework during the last couple of weeks and love every aspect of it. Except for the docs, which are pretty thin in some areas.

Either NestJS doesn't really have an opinion on how to implement "Event Sourcing", or I'm missing something obvious.

My main question is: What's the easiest way to persist the events themselves?

Right now, my events look pretty basic:

import { IEvent } from '@nestjs/cqrs';

export class BookingChangedTitleEvent implements IEvent {
    constructor(
        public readonly bookingId: string,
        public readonly title: string) {}
}

My initial idea was to use TypeORM (https://docs.nestjs.com/recipes/sql-typeorm) and make each of my events not only implement IEvent, but also make it inherit a TypeORM @Entity().

But that would have one table (SQL) or collection (NoSQL) for each of the events, making it impossible to read all events that happened to a single aggregate. Am I missing something?

Another approach would be to dump each event to JSON, which sounds pretty easy. But how would I load the object IEvent classes from the db then? (sounds like I'm implementing my own ORM then)

like image 541
user2838198 Avatar asked Dec 23 '18 18:12

user2838198


2 Answers

So I'm doing something similar and using postgres, which does support json ('simple-json') in TypeORM vernacular (reference). For better or worse, my event entity looks like:

@Entity()
export class MyEvent {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  name: string;

  @Column('simple-json')
  data: object;

  @CreateDateColumn({type: 'timestamp'})
  created_at: Date;
}

It's important to note that I'm only using my persisted events for an audit trail and the flexibility of potential projections I'm not already building. You can absolutely query on the JSON in postgres using TypeORM, eg. .where('my_event.data ::jsonb @> :data', {data: {someDataField: 2}}), but my understanding is querying your events to get current state is kinda missing the point of CQRS. Better off building up aggregates in new projection tables or updating one huge projection.

I'm fine with how I'm currently persisting my events, but it's certainly not DRY. I would think extending a base class with a common saveEvent method or using a EventHandlerFactory class that would take the repository in its constructor would be a bit cleaner, rather than injecting the repository into every handler.

Maybe someone out there has some good thoughts?

like image 154
Zach Avatar answered Nov 06 '22 00:11

Zach


First of all, your initial hunch was correct: NestJS CQRS module has no opinion on how you implement Event Sourcing. Reason is CQRS is something different than ES. While you can combine them, this is entirely optional. Then if you decide to go with ES, there are again a ton of ways to implement.

It seems you would like to persist your events in a relational database, which can be a good choice to avoid the additional complexity of having a second NoSql database (you can switch to a dedicated db later, e.g. Eventstore, and benefit from specialized ES features).

With regards to your SQL model, it is best-practice to have a single table for storing your Events. A very nice article that demonstrates this is Event Storage in Postgres by Kasey Speakman.

The Events table layout chosen here looks as follows:

CREATE TABLE IF NOT EXISTS Event
(
    SequenceNum bigserial NOT NULL,
    StreamId uuid NOT NULL,
    Version int NOT NULL,
    Data jsonb NOT NULL,
    Type text NOT NULL,
    Meta jsonb NOT NULL,
    LogDate timestamptz NOT NULL DEFAULT now(),
    PRIMARY KEY (SequenceNum),
    UNIQUE (StreamId, Version),
    FOREIGN KEY (StreamId)
        REFERENCES Stream (StreamId)
);

The article provides clear description of the rationale for each of the columns, but you can build your aggregates using a query based on the StreamId + Version. The Meta column can hold metadata, such as userId and correlationId (here's more info on correlation), etc. The article also mentions how you can create Snapshots, which in some cases may be handy (but avoid until needed).

Note the Type column, which stores the event type and can be used for deserialization purposes (so no need to create your own ORM ;)

Other projects that show how to implement event storage are PostgreSQL Event Sourcing and the more complete solution message-db.

like image 2
Arnold Schrijver Avatar answered Nov 06 '22 01:11

Arnold Schrijver