ADR-0002: Event Publication and Visibility for Cross-BC Boundaries
Status: Proposed
Date: March 2026
Context
Domain events in Protean are raised by aggregates and processed by event handlers within
the same bounded context. But some events need to cross bounded context boundaries — an
OrderShipped event in the ordering context may need to be consumed by the notification
context or the analytics context.
The framework needs a way for developers to declare which events are part of the domain's
"published language" — the subset of events that external consumers can depend on. This
distinction matters for several reasons: published events have stricter compatibility
requirements (changing them may break external consumers), they need to be routed through
external brokers (not just internal handlers), and the IR's contracts section should
catalog them for documentation and contract validation.
The question is what terminology and mechanism to use for this declaration.
Decision
We will use the published option on the event decorator to mark events that cross bounded
context boundaries:
@domain.event(part_of=Order, published=True)
class OrderShipped(BaseEvent):
order_id: Identifier(identifier=True)
shipped_at: DateTime()
The term "published" is grounded in Eric Evans' Published Language pattern from the DDD
blue book. A published event is one that the bounded context commits to maintaining as a
stable contract for external consumers. The default is published=False — events are
internal unless explicitly declared otherwise.
In the IR, published events appear in two places. First, inline on the event element within
its cluster ("published": true), where it's immediately available during element-by-element
processing. Second, in the top-level contracts section, which provides a derived summary
of all published events for consumers that only need the contract surface (API gateways,
documentation generators, schema registries).
The contracts section is derived — both representations come from the same developer
declaration. This dual approach avoids forcing consumers to choose between scanning every
cluster for published events or reading a separate contracts section.
Only events can be published. Commands are always internal to the bounded context. External happenings arrive as messages at the boundary via subscribers (acting as an anti-corruption layer) and get translated into internal commands. This is a DDD principle: commands are imperative ("do this"), events are factual ("this happened"). External systems react to facts, not imperatives.
Consequences
The published flag provides a clear, declarative mechanism for marking events as external
contracts. No static analysis of handler bodies or broker configurations is needed (see
ADR-0000, principle 2).
The IR's contracts section enables contract validation tooling (Phase 4). Diffing two IR
versions reveals which published events changed shape, allowing CI pipelines to flag
breaking changes before deployment.
The dual representation (inline + contracts section) means some information is present in two places. This is intentional — the inline flag serves element-level processing, the contracts section serves contract-level queries. Both are derived from the same source, so consistency is guaranteed by the builder.
The limitation is that published is a boolean — an event is either published or it isn't.
We considered using visibility with string values ("internal", "published",
"deprecated") for future extensibility. The visibility approach would allow finer-grained
control (e.g., marking an event as visible to specific consumers or deprecated-but-still-
supported). However, the simpler boolean covers the immediate need, and the compatibility
contract allows adding a visibility attribute alongside published in a
future minor version if richer semantics are needed.
This ADR remains in Proposed status because the exact mechanism for external broker
routing of published events is still under evaluation. The published declaration exists
in the framework, but the server's dispatch logic for routing published events to external
brokers is being refined.
Alternatives Considered
visibility with string values ("internal", "published", "deprecated") offers
more expressiveness but introduces vocabulary decisions (what other visibility levels might
exist?) and adds complexity for the common case (most events are internal, some are
published). We may revisit this if the boolean proves insufficient.
Per-command event causality (produces declarations on commands listing which events
they may raise) was considered for richer contract documentation. We rejected it because
handler logic is conditional — a PlaceOrder command may raise OrderPlaced or
OrderRejected depending on business rules. Static declarations of this kind drift from
reality as the domain evolves. The aggregate-to-events mapping (via part_of) provides
the coarser but always-accurate relationship.