CQRS Overview
Introduction
Section titled “Introduction”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.
Choose what you need to do
Section titled “Choose what you need to do”| 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 |
Before you start
Section titled “Before you start”You typically need:
- an
AppBuilderinstance; - 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.
Commands
Section titled “Commands”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:
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.
Add Command-specific features
Section titled “Add Command-specific features”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.
Queries
Section titled “Queries”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:
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.
Cache Query results
Section titled “Cache Query results”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.
Handlers
Section titled “Handlers”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.
Execute Commands and Queries
Section titled “Execute Commands and Queries”Commands and Queries are executed differently through the mediator.
Execute a Command
Section titled “Execute a Command”const result = await mediator.send(command, signal)Use send() for Commands.
Execute a Query
Section titled “Execute a Query”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.
Configure pipeline features
Section titled “Configure pipeline features”CQRS requests can use common pipeline features without putting that logic directly into every Handler.
Validation
Section titled “Validation”Enable validation when Commands or Queries need input validation.
See:
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:
Logging
Section titled “Logging”Use the logging pipeline to record Command and Query execution.
See:
Performance tracking
Section titled “Performance tracking”Use the performance pipeline to detect slow Command and Query executions.
See:
Configure Performance Tracking
Exception handling
Section titled “Exception handling”Use the exception pipeline to handle errors raised during request execution.
See:
Idempotency
Section titled “Idempotency”Use idempotency when a Command must not be processed more than once for the same request ID.
Configure it under:
config.commandBus.idempotencySee:
Idempotency applies to Commands.
Concurrency
Section titled “Concurrency”Use concurrency retry handling when Commands can fail because of concurrency conflicts.
Configure it under:
config.commandBus.concurrencySee:
This feature applies to Commands.
Caching
Section titled “Caching”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:
A typical CQRS workflow
Section titled “A typical CQRS workflow”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 ResultFor 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 resultFor a Command:
Create Command │ ▼Create Command Handler │ ▼Configure Command pipelines │ ▼mediator.send(...) │ ▼Command Handler │ ▼ResultWhere to go next
Section titled “Where to go next”Choose the guide that matches your task:
Create a Command
Section titled “Create a Command”Use this when you need to define an operation that changes application state or performs an application action.
Create a Query
Section titled “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
Section titled “Create a Handler”Use this when you need to implement or register the Handler for a Command or Query.
Configure application pipelines
Section titled “Configure application pipelines”Go to the relevant pipeline guide:
- Enable Validation
- Configure Logging
- Configure Performance Tracking
- Configure Idempotency
- Configure Concurrency
- Enable Caching
- Handle Exceptions
Support Us
Section titled “Support Us”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
