For the complete documentation index, see llms.txt. This page is also available as Markdown.

CQRS & Events

SoapJS ships first-class CQRS — commands, queries and domain events — wired through your DI container by decorators. It's opt-in: enable it with bootstrap({ cqrs: true }). For simple features you can skip it entirely and use a plain controller → use case → repository flow.

  • Commands change state (create/update/delete) and return a Result.

  • Queries read state and return a Result.

  • Domain events are facts ("something happened") that any number of consumers can react to.

Commands

Define a command (a message) and a handler:

import { BaseCommand } from '@soapjs/soap/cqrs';
import { CommandHandler } from '@soapjs/soap-express/cqrs';
import { Inject, Result } from '@soapjs/soap';

export class CreateUserCommand extends BaseCommand {
  constructor(public readonly email: string) { super(); }
}

@CommandHandler(CreateUserCommand)
export class CreateUserHandler {
  constructor(@Inject('UserRepository') private readonly repo: UserRepository) {}

  async handle(cmd: CreateUserCommand): Promise<Result<User>> {
    // ...validate, build the entity, persist
    return Result.withSuccess(user);
  }
}

Queries

Wiring & dispatching

With cqrs: true, bootstrap() binds a CommandBus and a QueryBus in the container and registers every decorated handler. Handlers register when their files are imported, so make sure they're loaded (e.g. imported in your composition root) before bootstrap runs.

Domain Events

Define events as facts (extend BaseDomainEvent) and react with @EventHandler:

A command handler publishes events after it succeeds (inject your event bus and call publish).

Multiple handlers per event (fan-out). Since @soapjs/soap-express 0.3.1 the default DI token is EventHandler:<eventName>:<handlerClass>, so several handlers can subscribe to the same event without overwriting each other. Pass { token } to override it.

Events are not auto-wired. bootstrap({ cqrs: true }) wires the command and query buses, but @EventHandler only records consumers in DecoratorRegistry — it does not connect them to a bus. You wire the event bus yourself (this is intentional: you choose in-memory, Kafka, RabbitMQ, etc.).

A minimal in-memory wiring, mirroring wireCqrs:

Read models & projections

Domain events are the natural way to keep a read model in sync: a projector consumes events and updates a denormalized store; queries read the projection instead of aggregating the write store on every request. See the demo for a complete commands → events → projection → WebSocket flow.

Guidelines

  • One handler per command/query; many handlers per event.

  • Keep handlers thin — orchestrate the domain, return a Result.

  • Commands read-before-write on the primary for strong consistency; route read queries to a replica if you use a read/write split.

Last updated