Entity-composed registrars

Table of Contents

Summary

An entity-composed registrar is a top-level, per-transport registration entry point that delegates to one sub-registrar per entity. Each sub-registrar is generated from the entity model and includes only that entity's own handler/controller, so a compile or wiring failure in one entity cannot block registration of the rest — the blast radius of a bad entity is a single translation unit, not the whole transport surface. The top-level registrar holds no entity-specific knowledge; it simply calls each register_<entity>_handlers() (or the transport's equivalent) and collects the results. The pattern recurs across transports: NATS today, HTTP and others to follow, each an instance of the same shape backed by its own codegen archetypes.

Detail

The meta-pattern

The shape has three participants:

  1. Per-entity sub-registrar — a generated <entity>_registrar unit (header + implementation) that knows how to wire exactly one entity's transport surface to that entity's handler. It includes only its own entity's handler header, keeping the dependency graph narrow.
  2. Top-level registrar — a single, hand-maintained (or separately generated) entry point per transport that delegates to every sub-registrar and aggregates their subscriptions/routes. It is entity-agnostic.
  3. Entity model — the single literate source the sub-registrar is projected from, via a codegen facet.

Why compose rather than emit one monolithic registrar:

  • Failure isolation — one entity's broken handler fails to compile its own <entity>_registrar only; the others still build and register.
  • Incremental change — regenerating one entity touches one pair of files; review and blame stay local to the entity.
  • Uniformity — every entity is wired the same way, so the wiring is a codegen concern, not a hand-maintained list that drifts.

Per-entity wiring is controlled by flags lifted from the entity model's C++ ** Flags drawer (e.g. no_history, list_only), so read-only or history-less entities omit the operations they do not support without any hand-editing of generated code.

Instance: NATS

The NATS instance generates, per entity, a <entity>_registrar.hpp/cpp pair in ores.<component>.core/messaging/. The header declares

std::vector<ores::nats::service::subscription>
register_<entity>_handlers(ores::nats::service::client& nats,
                           ores::database::context ctx,
                           std::optional<ores::security::jwt::jwt_authenticator> verifier);

The implementation constructs the entity's NATS handler and queue-subscribes its subjects (get_<plural>, save_<entity>, delete_<entity>, and — unless no_historyget_<entity>_history) to the handler's list/save/remove/history methods, returning the subscriptions for the top-level registrar to own. Entities without a NATS handler (e.g. some aggregates managed through a parent) are excluded and have no sub-registrar.

Generated by the nats-sub-registrar codegen facet:

Instance: NATS event-mapping

A second NATS instance, distinct from the request/response sub-registrar above: it wires an entity's Postgres LISTEN/NOTIFY channel to its changed-event, republishing to NATS. Generates, per entity, an <entity>_event_registrar.hpp/cpp pair — but in the component's .service layer, not .core, because the pipeline depends on ores.eventing.core (postgres_event_source, event_bus), which no .core component links. The header declares

[[nodiscard]] ores::eventing::service::subscription
register_<entity>_event_mapping(ores::eventing::service::postgres_event_source& event_source,
                                ores::eventing::service::event_bus& event_bus,
                                ores::nats::service::client& nats);

The implementation registers the entity's Postgres channel (product_component_entity_plural, matching what the notify_trigger SQL archetype emits) and subscribes the resulting event, republishing it via the shared ores::eventing::service::publish_entity_event helper. Before this facet existed, this wiring — one #include + one register_mapping<T>() call + one event_bus.subscribe<T>() lambda per entity — was hand-maintained directly in each component's application.cpp, and had already caused two missed-wiring regressions (party_type, purpose_type) before being replaced.

Generated by the nats-event-registrar codegen facet:

Instance: HTTP

(Planned.) The HTTP instance will follow the same shape: a generated per-entity registrar that mounts the entity's routes onto a router and delegates to the entity's HTTP handler, with a top-level registrar aggregating them. To be filled in when the HTTP sub-registrar facet and its archetypes exist; link them here as for NATS.

See also

Emacs 29.3 (Org mode 9.6.15)