Skip to content

Query Bus

The Query Bus is the core mechanism in DomusJS for handling read operations.

✅ Separates the query requester from the query handler.
✅ Supports CQRS (Command Query Responsibility Segregation) by splitting read and write pipelines.
✅ Allows adding middleware (e.g., caching, logging) transparently.


Without a query bus, you would call repository methods or services directly, making the system tightly coupled.

With the query bus:

  • You ask a query.
  • The bus finds the registered handler.
  • The handler returns the result.

Example:

const user = await queryBus.ask(new GetUserByIdQuery(userId));

✅ The requester only cares about what they want, not how it’s fetched.


To use the Query Bus, you need to:

✅ Ensure the QueryBus implementation has been initialized (by default, InMemoryQueryBus).
✅ Register each Query and its associated QueryHandler.

ℹ️ Note:
The QueryBus should have been initialized earlier by calling registerDomusCore().
You can then safely resolve it from the container using container.resolve().

get-user-by-id.query.ts
import { Query } from '@domusjs/core';
export class GetUserByIdQuery implements Query {
static readonly TYPE = 'GET_USER_BY_ID';
readonly type = GetUserByIdQuery.TYPE;
constructor(public readonly userId: string) {}
}
get-user-by-id.handler.ts
import { QueryHandler } from '@domusjs/core';
import { GetUserByIdQuery } from './queries/get-user-by-id.query';
interface User {
id: string;
name: string;
email: string;
}
export class GetUserByIdHandler implements QueryHandler<GetUserByIdQuery, User> {
async execute(query: GetUserByIdQuery): Promise<User> {
// Fetch and return the user
}
}
import { container } from 'tsyringe';
import { GetUserByIdQuery } from './queries/get-user-by-id.query';
import { GetUserByIdHandler } from './handlers/get-user-by-id.handler';
// Resolve the QueryBus (already set up by registerDomusCore)
const queryBus = container.resolve<QueryBus>('QueryBus');
// Register the handler
queryBus.register(GetUserByIdQuery, container.resolve(GetUserByIdHandler));
const user = await queryBus.ask(new GetUserByIdQuery('123'));

✅ The requester focuses only on what they want, not how it’s retrieved.

✅ Middleware and tracing can be added without modifying app logic.

✅ Use tsyringe for dependency injection.
✅ Register all handlers once at application startup.


DomusJS defines the following interfaces:

export interface Query<R = any> {
readonly type: string;
}
export interface QueryHandler<Q extends Query<R>, R = any> {
execute(query: Q): Promise<R>;
}
export interface QueryBus {
ask<Q extends Query<R>, R = any>(query: Q): Promise<R>;
register<Q extends Query<R>, R = any>(queryClass: { TYPE: string }, handler: QueryHandler<Q, R>): void;
}

✅ You can build your own QueryBus by following this interface.


While DomusJS ships with InMemoryQueryBus, you can implement custom solutions.

In complex architectures—especially in microservices or modular monoliths—you may need to resolve queries across service boundaries. DomusJS is designed to support this use case by allowing you to swap the default in-memory QueryBus with a distributed implementation.

This is useful when:

  • Queries must be delegated to external services or subsystems
  • You want to isolate bounded contexts with strict read separation
  • You’re integrating with legacy systems or public APIs