0046 — Service communication and composition
Status: Accepted. Date: 13 October 2026.
All services follow the same communication and dependency injection boundaries. Business commands, queries and integration events between services use versioned, typed Protobuf messages over RabbitMQ. Driving adaptors translate wire contracts into context-owned application types. This keeps transport and framework concerns outside domain and application code.
Browser-facing HTTP, external integration protocols and private health and diagnostics endpoints remain at their designated boundaries. Browser updates use the server-authorised Centrifugo protocol defined in CLIENT-SUBSCRIPTIONS.md.
Dependency registrations and lifecycle hooks belong in service composition roots. Application handlers receive explicit, provider-independent ports. Go composition uses Fx; other languages implement the same boundaries with idiomatic tooling. Worker cancellation and draining precede resource disposal, including after partial startup failure.
Broker topology is provisioned through administration tooling. Runtime roles have scoped permissions. Publications use durable messages, mandatory routing and publisher confirms. Queries use correlated request/reply messages; the consumer acknowledges a query delivery only after its reply is confirmed and routable. Uncertain reads can be repeated. Command and event consumers acknowledge after the owner transaction commits state, receipts, recorded outcomes and outgoing intents. Retries preserve message identity and use bounded retry, dead-letter and explicit replay policies.
Contexts without owned storage declare persistence: none. PostgreSQL remains
the default for persisted contexts. Administration and migration generation omit
storage-free registrations. This extends decision 0034, “fresh service-first
skeleton”.
These rules apply across services. Context documentation and published contracts specify individual operations, payloads and implementation status. Architecture checks and transport integration tests verify the shared boundaries and delivery guarantees as each service adopts them.
Beto adoption — 14 October 2026
Beto groups shared implementations under adaptors/{persistence,messaging,realtime,http}.
Context adaptors group repositories under persistence and wire mappings under
messaging. Foundation packages contain inward-facing primitives and technical
configuration, diagnostics and cancellation helpers. Architecture fixtures reject
context imports in shared adaptors, boundary drivers in foundation, database
drivers in delivery, and network delivery drivers in persistence.
Generated Beto outputs have explicit paths. Its selective generator includes only
the service's envelopes and owned request contracts; generation checks compare
missing, changed and obsolete outputs without changing source. Pinned Staticcheck
and errcheck join formatting, vet and architecture checks in pnpm check:beto and
pnpm verify.
The Project query manifest generates exact broker resource and topic permissions. Runtime has no configure permission, queue write permission or exchange read permission. A separate topology user receives the registered resources and exact binding keys with no shared-topic publication permission. Dead-letter declaration requires read on the query queue and write on its delivery exchange; those capabilities belong to the administration process. The host retains management provisioning authority and preserves runtime credentials. Each administration pass issues a fresh bootstrap credential; it is excluded from workload secret mounts. Reconnecting managed clients applies changed broker authorisation.
This adopts the applicable improvements through reference commit ed737e1.
Project's read-only model has no aggregate or domain events. The initial
adoption deferred command infrastructure; the foundation adoption below
supersedes that deferral. Wire contracts, migration bytes and acknowledgement order
are preserved. See the Beto adaptor map.
Beto query registration — 15 October 2026
Comparison through reference 3c8b080 adds scoped query/reply identifiers generated
from compiled Protobuf payload oneofs. The composition root registers each named
application handler with its owner input and reply mappings. A provider-independent
QueryBusPort carries plain query/result values; the shared messaging adaptor owns
dispatch and wire validation. Registration fails before startup for invalid or
foreign identifiers, missing callbacks, duplicate handlers and contract mismatches.
Architecture fixtures enforce composition ownership and generated identifiers.
Broker consumers also require envelope visibility to match the registered subscription. Existing private-owner checks and acknowledgement order remain. No message identity, schema, queue, migration or Project result changes. The initial query adoption deferred durable command reactions and explicit realtime buses; the foundation adoption below supplies those capabilities.
Beto schema-owned query contracts — 15 October 2026
Reference a9d7bf6 adds schema-generated plain inputs, responses, checked encoders
and response-bound registrations. Beto adopts these for Project. A Protobuf
message option declares each public query's owner and success response; generation
validates the declaration against its owner query/reply envelopes before writing
output. Unsupported shapes and invalid ownership/visibility fail generation.
Plain models live in contracts/requests/generated/model/<owner> and contain no
Protobuf runtime dependencies. Application handlers may import their own context's
plain models; published read-model names may alias them. Domain imports remain
independent of contracts. Owner encoders and typed registrations live in separate
packages under the context's adaptors/messaging/generated. Composition uses the
generated registration to bind the exact handler result and checked encoder.
Handlers select success or an application request error. Encoders preserve optional presence and reject ambiguous responses and integers outside the cross-language safe range. Shared dispatch redacts encoding failures and supplies the validated request identity when constructing the final reply. Complete-list behaviour, nullable legacy fields, queue identities and acknowledgement order are retained. Pre-change request/reply byte fixtures verify wire compatibility. Negative architecture fixtures reject wire imports into application, contract imports into domain, duplicated public shapes and handwritten query registration.
Beto command and delivery foundation — 16 October 2026
Complete the applicable Storefront infrastructure adoption through a9d7bf6.
Plain command handlers receive generated values, a narrow aggregate target, a
per-invocation owner write repository and an explicit realtime bus. Composition
binds handlers directly to the shared transaction coordinator. Remove the earlier
callback store, command port, generic Fact and HTTP command/query helpers.
The coordinator commits aggregate state, version, receipts, outcome, private facts, explicit realtime intent and public command reply bytes atomically. Private reactions commit command bytes before a paired consumer executes them. Integration publication and realtime subscribers own separate receipt/outbox transactions. Source revisions, times, IDs and exact saved bytes survive retry. Subscriber completeness, private codec pairing, retries and replay are checked against generated declarations before workers start.
Shared plain request values come from Protobuf. Required presence applies before dispatch and while encoding replies. Event declarations generate plain values, codecs and names. Application architecture checks use Go type information to reject transaction callbacks, handler chaining and aggregate mutations outside command handlers. The historical compatibility gate is independent of generation drift. The API generates Project's mechanical translation from its explicit ACL.
Keep Pharos UUID derivation, queues, query wire fields, HTTP output and frozen SQL baselines. Add Beto's checksummed delivery migration separately, through the administrator only. Project stays read-only and storage-free; its command, event and realtime registries remain empty. Project-specific provisioning is retained. Technical fixtures verify commands and recovery using isolated native PostgreSQL; no demonstration context enters production composition.
Durable publication back-off — 24 October 2026
Beto, Metallo and Themelio use the same deployment-independent retry policy for
persisted event, private-command, contact-projection, reply and realtime outboxes.
The first failure waits one second. Each subsequent lease generation doubles the
window up to five minutes, with equal jitter between half the window and its cap
and a one-second floor. The existing persisted generation counts abandoned claims
as attempts; available_at persists the sampled deadline under the current lease
fence. Restarting a relay preserves both. Successful completion retains the record;
a new publication starts its own retry sequence. No schema or wire change is needed.
Unroutable mandatory publications remain pending until the intended consumer route is provisioned. They have no automatic discard threshold. A confirmed broker return leaves the channel usable, so the relay schedules that record and continues with other due work. Missing consumers must be resolved through their owning composition; an unowned sink queue would conceal the missing delivery contract. Publisher confirms, mandatory routing, immutable bytes, receipts and access controls remain required. Replies retain their existing expiry, which also caps their next deadline.
Publication-worker connection and database failures use a separate one-to-thirty second jittered back-off, reset after a successful operation. Opening a socket alone does not reset it. Cancellation interrupts waits. These defaults apply to production and development alike; normal first delivery remains immediate. The five-minute cap reduces sustained outage traffic while permitting automatic recovery. Business workflow retries, bounded consumer dead-letter transfer and API request deadlines retain their separate policies.
Real PostgreSQL checks cover persisted deadlines, restart recovery, stale leases, unchanged material, fresh-record progress and reply expiry. Broker qualification checks mandatory returns followed by successful delivery on the same connection. This controls retry amplification; storage latency still requires independent capacity and durability monitoring.
API reply isolation — 16 October 2026
API connections own temporary exclusive classic reply queues. Each connection
supplies an owner-scoped UUIDv4 reply address in AMQP ReplyTo; Beto validates it
before dispatch. Query replies use that address with mandatory publication and
confirms. Command transactions save the address with immutable reply bytes, and
recovered dispatches preserve both. Reusing a request identity with another
address is a conflict; retrying a command through a new connection uses a fresh
request ID and the same command identity. Existing stored replies and requests
without ReplyTo retain the legacy owner route.
This introduces one narrow exception to administrator-provisioned topology: API runtime credentials can declare and bind their temporary reply queue family. Owner workers retain no configure permission. Topic permissions constrain both API bindings and owner publications; wildcard and foreign-owner addresses fail closed. Administrators still provision all exchanges and durable queues. Queue lifetime follows the API connection; disconnection releases waiting calls and retries use a new connection address. See the Project transport contract for the compatibility route, rollout order and real broker qualification.
API commands and authenticated actors — 16 October 2026
Bindings receive the authorised HTTP request and resolved operation schema.
CommandBinding maps POST path identity, idempotency/version/correlation headers
and validated JSON into an owner-typed Protobuf request. The broker client supports
both command and query routing. API tests compile the existing technical fixture
into an independent API module; production Project remains query-only.
The additive CommandMetadata.actor_id field carries the authenticated subject.
The server supplies a distinct actor for CLI credentials. Beto passes the subject
through plain metadata to CommandContext.ActorID; business handlers can apply
owner policy without HTTP dependencies. A present actor contributes to receipt
fingerprints, preventing replay under a different user. Absent actors retain
legacy/internal compatibility and grant no authenticated-user authority. New HTTP
commands require an actor. A pre-upgrade receipt retried with a new actor conflicts;
its uncertain outcome must be resolved before submitting a new command identity.
Themelio Python adoption — 16 October 2026
The Python scaffold follows the reference Operations foundation, including connection-specific reply destinations and authenticated command actors. Its plain metadata and handler context carry the actor; receipt fingerprints include present actors. Administrator-only, checksummed service migrations retain reply routes alongside immutable bytes. Empty production registries grant no context or broker authority. Test-only request contracts exercise the complete transport and transaction path. See the Themelio guide.
Contexts may share one physical PostgreSQL instance while retaining separate logical databases and credentials. Shared deployment provisioning and live qualification are covered by PR #697. API generation and broker provisioning remain Project-specific and must be generalised when adding the next context.
Beto request catalogue — 21 October 2026
contracts/beto/messaging/catalogue.json declares each Beto request contract with
its Protobuf file and consumers; only the API consumes them for now. Beto request
bindings, API consumer bindings and API HTTP mappings are generated for every
declared owner in its own namespace. Drift checks report missing, changed and
obsolete outputs. A contract may be declared before its context is registered; it
then receives no broker user, queue or credential. This replaces the
Project-specific generation and provisioning recorded above.
Broker authority stays explicit. An owner that offers requests needs a catalogue
entry and must name its callers in broker.json. A missing caller list fails
instead of opening every offered kind to the API. Until Beto authenticates request
callers it accepts only the API's identity and reply route, so the API is the only
permitted caller. A Beto registration may declare "messaging": "none", like
persistence; it then cannot declare broker contracts, events or subscriptions. An
owner without requests, event publications or subscriptions receives no broker
user. The generated runtime_bindings name each principal's credential, with
pharos_<name> using <NAME>_BROKER_URL. Host provisioning, the Argo deployment
check and integration bindings read them. The administration image packages every
owner's contracts and Beto's contexts.
Project's wire contracts, queues and effective permissions are unchanged; its
generated permission patterns differ only in grouping. Test owners live under
scripts/fixtures/ and are activated in temporary trees, so no demonstration
context enters production registries. See adding a context.
Governance reconciliation — 23 October 2026
The generic catalogue and process manifest now include Party Governance and the persistent Project context. Existing message identities and field numbers survive their move into role-separated files. API Go and Metallo Python bindings derive from the same declarations; binding generation grants no deployed consumer access.
Governance authenticates broker requests using RabbitMQ-verified UserId and
generated owner-specific caller policies. An owner must declare every caller.
allow_actor defaults to false; only the API can enable it or send the existing
private authentication/provider-binding protocols. A caller's identity and reply
family remain consistent across owners, while its kinds and actor authority are
scoped per owner. Production enrolment remains API-only. This supersedes the
earlier pre-authentication restriction in the catalogue adoption above.
Party and Project retain separate PostgreSQL databases, credentials, migration histories and durable delivery. Their private protocol workers are covered by the runtime-owner manifest and real broker composition checks. The source worker and unconnected downstream consumers remain paused.
Beto owner checkpoints — 25 October 2026
Beto publishes recoverable Project and Party views for Execution Core, Pipeline Coordination and Workload Portfolio. Each consuming context has one authenticated principal across both owners and independent durable event, retry and dead-letter queues. Its query permissions are limited to the authority and synchronisation operations declared by each owner. The restricted all-Party contact inventory is reserved for an explicitly enrolled support caller.
A consumer binds its queues before asking an owner to materialise a checkpoint. The owner uses a repeatable-read transaction to save immutable items, exact revisions, deterministic hashes and an ordered manifest. It commits a checkpoint marker to a dedicated outbox in the same transaction. Pages are stable while the checkpoint is valid; an expired checkpoint restarts from the beginning. Project inventory is scoped to the enrolled provider, instance and source scope. Party bootstrap selects canonical IDs discovered in Projects, while the support inventory covers Parties outside Projects.
A checkpoint marker is advisory. RabbitMQ ordering applies within one queue and
channel, with no ordering guarantee across queues, owner streams or reconnects.
A consumer can claim convergence only after its exact IDs, independent revisions
and hashes match the owner checkpoint and every relevant buffered event through
those revisions has been applied. ProjectChanged and ProjectAccessChanged
retain separate receipts even when they share a Project revision. Authentication,
provider-binding and contact revisions are also independent. Duplicate or older
facts cannot replace newer stream state; retirement and removal evidence remains
part of comparison.
The same subscription-before-snapshot procedure supports initial load, daily comparison, repair and recovery after local data loss. Counts are diagnostic only. Synchronisation proves agreement about the published shared fields. Current permission and continuing-Run authority remain separate owner decisions. This section supersedes the API-only enrolment state recorded on 23 October 2026.
Page source
Source snapshot · Revision f928834c8.