Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

DTOs Returned by Queries

A query's caller wants { customerId, name, plan, openInvoices, lastPaymentAt, status } in a single round-trip. The aggregate does not have that shape — it has internal entities, value objects, mutable state. A read model from @frenchexdev/ddd-read-model is the denormalised, query-shaped DTO the query handler returns instead.


What @ReadModel Reifies

The CQRS split between writes and reads is meaningful precisely because the shapes are different. The write side has the aggregate — invariants, identity continuity, transactional boundary. The read side has the read model — flat, indexed, optimised for the query that needs it. The read model is regenerable from the event stream; the aggregate is the source of truth.

The decorator's purpose is to make read models visible. Without it, a read model is just a type declaration the codegen and the analyzer cannot see. Decorating it lets the codegen tie the read model to its feeding projection (every read model must be populated by at least one projection) and validates that query handlers do not return raw aggregates (which would defeat the CQRS separation).


The Runtime: ddd-read-model

M4/M5 stub. The decorator surface will declare the read model's name and the projections that populate it. The current shape is a stub pinned to ReadModelCqrsRequirement.

A sketched future shape:

// Sketch — runtime decorator not yet exported.

@ReadModel({
  name: 'CustomerDashboardReadModel',
  populatedBy: ['CustomerDashboardProjection'],
})
export interface CustomerDashboardReadModel {
  readonly customerId:    CustomerId;
  readonly name:          string;
  readonly plan:          BillingPlanId | null;
  readonly openInvoices:  number;
  readonly lastPaymentAt: string | null;
  readonly status:        'active' | 'suspended' | 'cancelled';
}

The interface declaration shape (rather than a class) is the typical case — read models are pure data, no methods, no behaviour. The decorator targets the interface (or a marker class) so the codegen has a symbol to attach metadata to.


  • Returned by @QueryHandler — the query handler's signature names a read model as its response type.
  • Populated by @Projection — every read model declares at least one feeding projection.
  • May be indexed by @Search for full-text queries or cached by @Cache for fast retrieval.
  • Distinct from @AggregateRoot — write side has the aggregate, read side has the read model.

Back to the series index.

⬇ Download