Files
clean-architecture-backend-…/docs/adr/ADR-WS-003-stomp-and-broker-relay.md
T

75 lines
4.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ADR-WS-003: STOMP is an Advanced adapter with a declared destination catalog
- Status: Accepted
- Date: 2026-08-25
- Scope: `adapter:inbound:websocket``advanced.stomp`, `advanced.stomp.rabbit`
## Context
Advanced Tasks 913 add STOMP 1.2 alongside the platform's own protocol, plus a RabbitMQ broker
relay and cross-node user destinations.
This leaf already ships an older STOMP-over-SockJS channel (`stomp`, gated on
`ca-skeleton.websocket.enabled`). Two `@EnableWebSocketMessageBroker` configurations in one context
do not conflict loudly — both contribute a configurer, both call `configureMessageBroker`, and the
broker that results is whichever ran last. Nothing errors and nothing logs.
STOMP also brings a destination model that is a free string from the client. Without a catalog, the
set of reachable destinations is whatever the broker accepts, which for the simple broker is every
string.
## Decision
**The Advanced adapter is its own module (`advanced-stomp`), separate from `advanced`.** It is the
one Advanced capability that cannot be pure — STOMP here *is* the Spring Messaging types — and
folding it into `advanced` would relax that module's purity for every capability in it.
**The relay is a further module (`advanced-stomp-rabbit`).** The adapter parses a protocol; the
relay opens a TCP connection to somebody else's broker and makes every delivery depend on it.
Different blast radius, different decision, different module.
**Destinations are declared, per operation.** `StompDestinationCatalog` maps `(operation,
destination)` to a required permission. Undeclared is refused. `SUBSCRIBE` and `SEND` are separate
declarations, because reading a feed and publishing into it are different rights.
**The authorization decision is a value, not an interceptor method.** `StompAuthorizationPolicy`
returns a `StompAuthorizationDecision`; `StompSecurityInterceptor` only extracts and enforces. A rule
reachable only through a `MessageChannel` gets tested for the cases somebody built a channel for.
**Only one STOMP runtime may run.** `StompBrokerExclusivity` fails the context when both channels
are enabled, when both brokers are, or when the adapter is enabled with no broker behind it.
**A `RECEIPT` is never promoted to a commit.** `StompEvidence` has six stages and
`StompAckPolicy.evidenceForReceipt()` is fixed at `PROTOCOL_RECEIPT`. The receipt is written by the
protocol layer, which knows nothing about whether the work succeeded.
**The simple broker declares what it cannot do.** `SimpleBrokerProfile` cannot be constructed
claiming cluster support or durable acks, and refuses activation outside local/test — in a
multi-node deployment it does not error, it delivers to whichever fraction of users is on the
publishing node.
**Unresolved user destinations are broadcast once and then dead-lettered.**
`MultiNodeUserDestination` distinguishes a message that arrived *via* the broadcast from one that did
not. Without that, every node rebroadcasts every unresolvable message on receipt.
## Consequences
- Enabling Advanced STOMP requires disabling the legacy channel. There is no migration path that
runs both; the exclusivity check makes that explicit at startup rather than at 3am.
- A deployment must write its own catalog. There is deliberately no default: an empty one refuses
every frame and reads as a broken adapter, and a non-empty one publishes destinations nobody chose.
- The relay's cost is one broker connection per authenticated session plus one system connection.
`brokerConnectionsFor` exists so this is computed before the first outage.
- User-destination metrics are tagged with `UserDestinationAction`, never the destination — a user
destination contains a user identifier by construction.
## Alternatives considered
- **Extend the existing `stomp` package.** Rejected: it is Stable, and WS-ARCH-6 forbids a Stable
module naming an Advanced one. Making the legacy channel profile-driven would have required that
edge.
- **One `advanced-stomp` module including the relay.** Rejected: the relay is a separate operational
decision and deserves to be refusable on its own.
- **Allow undeclared destinations with a wildcard permission.** Rejected: the wildcard becomes the
default and the catalog becomes documentation.