Phase 0a — ADRs (9 new): - ADR-026: Transport/interface separation (three-layer model) - ADR-027: Crate decomposition (core, secret, storage, flowgraph, napi, CLI) - ADR-028: Auth as irpc service (AuthProtocol behind feature flag) - ADR-029: Identity as core type (Identity + IdentityProvider in alknet-core) - ADR-030: Static/dynamic config split (ArcSwap, ConfigReloadHandle) - ADR-031: Forwarding policy (rule-based allow/deny, TransportKind-aware) - ADR-032: Event boundary discipline (domain, irpc, call protocol boundaries) - ADR-033: OperationEnv universal composition (three dispatch paths) - ADR-034: Head/worker terminology (replace hub/spoke) Phase 0b — New spec documents (7): - identity.md, services.md, interface.md, configuration.md, storage.md, flowgraph.md, secret-service.md Updated existing docs: - auth.md: reference identity.md for canonical definitions, add AuthProtocol - open-questions.md: resolve OQ-12, OQ-16, OQ-18, OQ-22, OQ-23-25 - README.md: add all new docs, ADRs 026-034 Marked 19 architecture tasks as completed.
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