Skip to content

CQRS Overview

Use the CQRS features in Xeno.JS to build application operations as Commands and Queries, and execute them through their corresponding Handlers.

The Application module provides the main building blocks you need:

  • Commands for operations that perform application actions;
  • Queries for operations that retrieve data;
  • Handlers for executing Commands and Queries;
  • Mediator for sending Commands and executing Queries;
  • Pipeline behaviors for cross-cutting concerns such as validation, logging, performance monitoring, caching, idempotency, concurrency, and exception handling.

If you already know which task you need to perform, use the corresponding guide below.

I need to… Go to
Create a Command Create a Command
Create a Query Create a Query
Create a Command or Query Handler Create a Handler
Add validation Enable Validation
Add logging Configure Logging
Track slow Commands and Queries Configure Performance Tracking
Add idempotency to Commands Configure Idempotency
Handle concurrency conflicts Configure Concurrency
Cache Query results Enable Caching
Handle exceptions Handle Exceptions

You typically need:

  • an AppBuilder instance;
  • a Command or Query;
  • a corresponding Handler;
  • the mediator to execute the request.

If you are building an application from scratch, start with Create a Command or Create a Query.

Use a Command when the application needs to perform an operation.

Examples include:

  • creating a user;
  • updating an order;
  • deleting a resource;
  • changing application state;
  • triggering an application action.

Create a Command by following:

Create a Command

The Command is then executed through the mediator:

const result = await mediator.send(command, signal)

The Command is associated with a Handler using its request intent.

For the complete registration and Handler workflow, see Create a Handler.

Commands can use the common application pipeline features, such as:

  • validation;
  • authorization;
  • logging;
  • performance monitoring;
  • exception handling.

Commands can also use Command-specific features:

  • idempotency;
  • concurrency retry handling.

For example, to enable idempotency:

const app = new AppBuilder()
.addPipeline((config) => {
config.commandBus.idempotency = {
lockTtlSeconds: 30,
processedTtlSeconds: 300,
}
})
.build()

See Configure Idempotency for the complete configuration.

For concurrency handling, see Configure Concurrency.

Use a Query when the application needs to retrieve data.

Examples include:

  • retrieving a user;
  • loading an order;
  • searching products;
  • loading a paginated list;
  • retrieving application data for a screen.

Create a Query by following:

Create a Query

A Query can be executed through the mediator:

const result = await mediator.query(query, signal)

The Query is associated with a Handler using its request intent.

For the complete Handler workflow, see Create a Handler.

Query caching is available through the Query pipeline.

Enable the Query pipeline together with a cache:

const app = new AppBuilder()
.addCache()
.addPipeline((config) => {
config.queryBus.isEnabled = true
})
.build()

A cacheable Query defines its caching options when it is created.

For example:

export class GetUserQuery extends Query<User> {
constructor(
public readonly userId: string,
) {
super('GetUserQuery', {
cacheKey: `user:${userId}`,
ttl: 60,
bypassCache: false,
consistentRead: false,
isUserScoped: false,
})
}
}

For the complete caching workflow, see Enable Caching.

For the complete Query creation workflow, including Query parameters and schemas, see Create a Query.

Every Command or Query needs a Handler that performs the actual application operation.

Create a Handler by extending BaseHandler and implementing executeAsync():

export class GetUserHandler extends BaseHandler<
GetUserQuery,
User
> {
protected async executeAsync(
request: GetUserQuery,
): Promise<ResultType<User>> {
// execute the application operation
}
}

The Handler receives the request and can use its dependencies to perform the operation.

See Create a Handler for:

  • extending BaseHandler;
  • injecting dependencies;
  • implementing executeAsync();
  • accessing the current user context;
  • registering the Handler;
  • connecting the Handler to a Command or Query.

Commands and Queries are executed differently through the mediator.

const result = await mediator.send(command, signal)

Use send() for Commands.

const result = await mediator.query(query, signal)

Use query() for Queries.

Both operations return the result produced by the corresponding Handler or by a pipeline behavior that completes the request before the Handler runs.

For example, a cached Query can return a cached result without executing its Handler.

CQRS requests can use common pipeline features without putting that logic directly into every Handler.

Enable validation when Commands or Queries need input validation.

See:

Enable Validation

The validation guide explains:

  • how to configure validation;
  • how to register Zod schemas;
  • how to add custom validation;
  • what happens when validation fails.

For creating the actual Query or Command schema, use:

Use the logging pipeline to record Command and Query execution.

See:

Configure Logging

Use the performance pipeline to detect slow Command and Query executions.

See:

Configure Performance Tracking

Use the exception pipeline to handle errors raised during request execution.

See:

Handle Exceptions

Use idempotency when a Command must not be processed more than once for the same request ID.

Configure it under:

config.commandBus.idempotency

See:

Configure Idempotency

Idempotency applies to Commands.

Use concurrency retry handling when Commands can fail because of concurrency conflicts.

Configure it under:

config.commandBus.concurrency

See:

Configure Concurrency

This feature applies to Commands.

Use query caching when repeated Query executions can reuse a previously stored result.

Enable the Query pipeline:

.addPipeline((config) => {
config.queryBus.isEnabled = true
})

and configure a cache:

.addCache()

Then define cacheOptions on the Query.

See:

Enable Caching

A typical application flow looks like this:

Create Command or Query
│
▼
Create its Handler
│
▼
Register the Handler
│
▼
Configure required pipelines
│
▼
Execute through the mediator
│
▼
Receive the Result

For a Query with caching:

Create Query
│
▼
Define cacheOptions
│
▼
Create Query Handler
│
▼
Enable Query pipeline + cache
│
▼
mediator.query(...)
│
├── cached result ──► return result
│
└── cache miss
│
▼
Query Handler
│
▼
store result
│
▼
return result

For a Command:

Create Command
│
▼
Create Command Handler
│
▼
Configure Command pipelines
│
▼
mediator.send(...)
│
▼
Command Handler
│
▼
Result

Choose the guide that matches your task:

Create a Command

Use this when you need to define an operation that changes application state or performs an application action.

Create a Query

Use this when you need to retrieve application data.

This guide also covers the Query-specific configuration used by features such as caching.

Create a Handler

Use this when you need to implement or register the Handler for a Command or Query.

Go to the relevant pipeline guide:


Xeno.JS is an MIT-licensed open source project. It can grow thanks to the support of these awesome people. If you’d like to join them, please read more at support section