Search code examples
nestjscqrsevent-sourcing

How to migrate a PUT endpoint to a command based app (event sourcing)


I'm almost done with implementing event sourcing using CQRS module of NestJS, but now there is something I don't know how to handle. Currently, I have cron jobs that send transactions hourly to a my PUT /transactions endpoint. The payload is like this:

{
    "status": "pending",
    "amount": 10000,
    "uid": "test123",
    "transaction_id": "test-transaction1234",
}

When the transaction is sent to the endpoint, I don't know which fields have changed yet. I have four posible commands: CreateTransactionCommand, UpdateAmountCommand, ApproveTransactionCommand, DeclineTransactionCommand. Two commands could occur at 'the same time', for example and UpdateAdmount and then an ApproveTransaction. The first one should aways occur at first.

How should I handle that? I was thinking about something like this, is it a good solution?

  @Put()
  async createOrUpdate(@Body() dto: Transaction) {
    try {
      await this.transactionsService.createTransaction(dto);
    } catch (e) {
      if (typeof e == ExistingTransactionError) console.error(e);
      else throw e;
    }
    try {
      await this.transactionsService.updateTransactionAmount(dto);
    } catch (e) {}
    if (dto.status === 'approved')
      await this.transactionsService.approveTransaction(dto);
    if (dto.status === 'declined')
      await this.transactionsService.declineTransaction(dto);
  }

Solution

  • Okay, I simply did a PUT endpoint, and thanks to the power of the aggregate root, it's pretty easy to know which command I can do.

      @Put()
      async createOrUpdate(@Body() dto: TransactionEventDto) {
        try {
          return await this.transactionsService.createTransaction({
            ...dto.payload,
            status: 'pending',
          });
        } catch (e) {
          if (!(e instanceof TransactionAlreadyCreatedError))
            throw new HttpException(e.message, HttpStatus.BAD_REQUEST, e);
        }
        try {
          const { transaction_id, amount, status } = dto.payload;
          await this.transactionsService.updateTransactionAmount(
            transaction_id,
            amount,
          );
          if (status === 'approved') {
            return await this.transactionsService.approveTransaction(
              transaction_id,
            );
          }
          if (status === 'declined') {
            return await this.transactionsService.declineTransaction(
              transaction_id,
            );
          }
          return;
        } catch (e) {
          throw new HttpException(e.message, HttpStatus.BAD_REQUEST, e);
        }
      }
    

    If the order matter, just use the 'sequential way' of promise (i.e await in chain)