Skip to content

ADR-0002: Keep read models with projections in application/projections

  • Status: superseded on 2026-10-12 by the kit: application/projections/ is a role of the canonical tree
  • Date: 2026-10-10
  • Deviates from: contexts.scheduling.extraDirectories.application/projections, contexts.notification.extraDirectories.application/projections, contexts.analytics.extraDirectories.application/projections

Three contexts keep read models up to date from events: scheduling (practitioners known from identity, the agenda from its own events), notification (contacts) and analytics (outcomes and occupancy). The kernel gives this a role of its own, @Projection, whose README places it in application/projections/*.projection.ts. The architecture reference lists a closed tree per context in which that directory does not appear, the checker refuses it (tree/closed), and the generated barrel leaves the classes out.

Projections live in application/projections/ of the three contexts, declared as an extra directory in the configuration, and the composition root adds them to the module of their context by name.

A read model is written by one named class that the kit makes idempotent and ordered, instead of several integration event handlers each sending a command. The barrel does not list these classes: a projection that is not named in src/platform/compose-application.ts is silently not subscribed, so the end-to-end specs of each read model are what proves the wiring. ddd-ts-kit docs does not list projections in CONTEXT.md.

Integration event handlers sending commands to an aggregate that is a read model (the supported form): more files, hand-written idempotency and ordering, and no rebuild. Putting the projection files in application/integration/handlers/ under another suffix: the role check refuses a class without the decorator of the directory.

The directory application/projections/ of scheduling, notification and analytics, and nothing else: files named *.projection.ts holding one class declared with @Projection. The rule is tree/closed.

The checker and the barrel know application/projections/: the three extraDirectories entries and the explicit registration in the composition root are then deleted. Checked at each upgrade of the kit by whoever upgrades it.

On 2026-10-12 the kit made the projection a role of its own: application/projections/*.projection.ts is a directory of the canonical tree, the generated barrel exports each projector and registers what its constructor takes, and ddd-ts-kit generate projection writes one. The removal condition above is met. The three extraDirectories entries and their deviations were deleted from ddd-ts-kit.config.json, the composition root no longer names the projections (contextModule('scheduling', schedulingDeclarations) brings them), and ddd-ts-kit docs lists them in the reference of each CONTEXT.md. One consequence in the tests: a handler spec of scheduling now assembles the agenda projection with the rest of the context, so it gives a fake to the read model the projection writes. This record is kept for the history of the decision; nothing in the configuration points to it any more.