Update four existing specs (overview, server, napi-and-pubsub, call-protocol) to reflect Phase 0 decisions: three-layer model, IdentityProvider, ForwardingPolicy, OperationEnv, static/dynamic config split. Review all 9 Phase 0a ADRs (026-034) for consistency. Fix 4 critical issues from architecture review: missing OQ-SVC-05 in open-questions.md, deprecated hub terminology, undefined AuthService and noq terms. Replace inline OQ text with cross-references per format rules. Add ConfigServiceImpl definition to configuration.md. Port absolute workspace paths to project-relative links by copying referenced docs (feasibility, certbot, fail2ban, event_source_types) into docs/research/.
4.5 KiB
ADR-032: Event Boundary Discipline
Status
Accepted
Context
The research identified three distinct communication patterns in the system, and conflating them is a known anti-pattern in event-driven architectures:
-
Domain events (Honker streams) — Internal to the service that owns that data. Used for state reconstruction within the service's own boundaries. Examples:
nodes:created,edges:deleted,accounts:updated. -
irpc service calls — Synchronous request-response within a node or cluster. Internal to the system. Examples:
AuthProtocol::VerifyPubkey,SecretProtocol::DeriveEd25519,ConfigProtocol::ReloadForwarding. -
Call protocol events (
EventEnvelope) — Asynchronous integration events that cross node boundaries. External to the system. Examples:call.requested,call.responded,call.completed,call.aborted.
Without a hard constraint, it's tempting to have one service subscribe directly to another service's Honker streams. This leads to:
- Leaky event store: Service A reads Service B's domain events directly, coupling A to B's internal state representation. When B changes its schema, A breaks.
- Boomerang coupling: An integration event is too thin, causing the consumer to call back to the source service synchronously to get details. This negates the benefit of async communication.
- Fat notification trap: A notification event carries full entity state, when it should use state transfer instead.
Decision
Event boundary discipline is a hard architectural constraint, not a suggestion.
-
Domain events stay within the owning service. A Honker stream published by the storage service (
nodes:created) is for the storage service's own state reconstruction. No other service reads these stream events directly. -
irpc service calls are synchronous and internal. They never cross node boundaries. They are request-response, not events. They should not be used as a substitute for integration events.
-
Call protocol events are the only events that cross node boundaries.
EventEnvelopeframes are the integration boundary. When a domain event needs to be communicated to another node, it must be projected into a call protocol event. -
Projection from domain events to integration events is required when crossing boundaries. A service that owns a Honker stream must project relevant state changes into
EventEnvelopeframes before they leave the node. The projection strips internal details and produces a versioned, stable integration event.
This discipline applies at three levels:
Call Protocol (Layer 3, external, JSON)
└── irpc Service (Layer 3, internal, postcard)
└── Honker Streams (Domain events, within service boundary)
A call protocol handler MAY call an irpc service internally (e.g.,
/head/auth/verify calls AuthProtocol::VerifyPubkey). The irpc service MAY
use Honker streams for its own state management. But domain events never
propagate beyond the service boundary without projection.
Consequences
- Positive: Prevents leaky event stores. Services are independently deployable and their internal schemas can evolve without breaking consumers.
- Positive: Honker and irpc are implementation details, not cross-boundary
contracts. The call protocol's
EventEnvelopeis the only stable, versioned contract that other nodes depend on. - Positive: Clear ownership. Each service owns its Honker streams and can change them freely. Integration events are a deliberate, reviewed contract.
- Positive: Makes testing easier. Services can be tested in isolation with
mock domain events. Integration events are tested against the
EventEnvelopeschema. - Negative: Projection code is required. Every domain event that needs to cross a boundary must be explicitly projected. This is deliberate — the overhead ensures the integration contract is intentional.
- Negative: Developers must resist the temptation to subscribe directly to Honker streams across services. Code review should catch this pattern.
References
- research/services.md — Event boundary discipline section
- research/storage.md — Honker integration, event boundaries
- research/integration-plan.md — ADR 032 entry
- event_source_types.md — Event-driven architecture patterns