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:
- Per-entity sub-registrar — a generated
<entity>_registrarunit (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. - 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.
- 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>_registraronly; 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_history — get_<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:
- Facet: ores.cpp.nats-sub-registrar
- Header archetype: ores.cpp.nats-sub-registrar.nats_registrar_header
- Implementation archetype: ores.cpp.nats-sub-registrar.nats_registrar_implementation
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:
- Facet: ores.cpp.nats-event-registrar
- Header archetype: ores.cpp.nats-event-registrar.nats_event_registrar_header
- Implementation archetype: ores.cpp.nats-event-registrar.nats_event_registrar_implementation
- Recipe: How do I wire an entity into live NATS eventing?
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
- Anatomy of a Service — where the top-level registrar sits in the service runtime.
- ores.cpp.nats-handler — the handler facet the NATS sub-registrar wires up.