Files
clean-architecture-backend-…/docs/superpowers/specs/2026-09-18-messaging-platform-bridge-design.md
T

95 lines
4.8 KiB
Markdown

# Messaging Platform Bridge Design
## Goal
Replace the application-specific broker seam with one canonical anti-corruption bridge:
```
application-core IntegrationEventPublishPort
-> adapter/outbound/messaging/platformbridge
-> messaging-schema-api EncodedMessagePublisher
-> messaging-runtime-core DefaultMessagePublisher
-> messaging transport/runtime
```
The bridge must preserve canonical event identity and exact encoded bytes while reusing the platform's destination resolution, authorization, admission, runtime leasing, transport normalization, and observation pipeline.
## Scope
This phase introduces and verifies the canonical bridge. It does **not** migrate the legacy outbox storage/relay rows, because `OutboxEvent` does not retain the schema/order/tenant metadata required to reconstruct `ValidatedIntegrationEvent` without invention.
## Application boundary
Create `IntegrationEventPublishPort` in `application-core`.
Signature:
```java
CompletionStage<OutboxPublishOutcome> publish(ValidatedIntegrationEvent event);
```
The application package depends only on its own canonical event model and application outcome vocabulary.
## Adapter bridge
`PlatformIntegrationEventPublishAdapter` lives under:
```
adapter/outbound/messaging/platformbridge
```
It depends on `EncodedMessagePublisher`, never on a concrete broker client, runtime-core implementation, or transport SPI.
Mapping rules:
- `logicalDestinationId` -> platform `DestinationName`.
- `contractId` -> platform `MessageType`.
- `payloadVersion` -> `SchemaVersion`.
- event and causation identities must parse as UUIDv7; values are preserved exactly. Incompatible identities fail closed before the platform publisher is called.
- `occurredAt` is used for both `producedAt` and `occurredAt` until the application canonical model carries a separate production timestamp. The bridge never invents a new timestamp.
- producer is an explicit constructor/configuration value.
- correlation, partition key, tenant, aggregate order and exact envelope bytes are preserved.
- trace context is explicitly absent (`TraceContext.none()`) until the application model owns canonical trace context.
- exact `envelopeBytes` become `EncodedMessage` bytes; no re-encoding occurs.
- schema/catalog/binding/envelope evidence that has no first-class platform field is preserved as bounded `x-ca-*` headers.
- the schema reference subject is the canonical contract id and version is the canonical payload version.
## Outcome mapping
Mapping is based on completion **and transmission evidence**, not enum name similarity:
- CONFIRMED -> `OutboxPublishOutcome.CONFIRMED`.
- AMBIGUOUS -> `OutboxPublishOutcome.AMBIGUOUS`.
- REJECTED + NOT_TRANSMITTED -> `REJECTED_BEFORE_SEND`.
- REJECTED + any evidence that bytes may have left the process -> `REJECTED_AFTER_BROKER`.
Bridge preparation failures are definite pre-send rejection.
## Platform boundary
`EncodedMessagePublisher` is owned by `messaging-schema-api`, because `EncodedMessage` is owned there and the dependency direction remains acyclic.
`DefaultMessagePublisher` implements both `MessagePublisher` and `EncodedMessagePublisher`. The encoded path skips only codec lookup/encoding; destination resolution, access policy, admission, runtime lease, transport send, deadline handling, result normalization and observation are shared with the normal publish path.
The starter exposes one `DefaultMessagePublisher` singleton, which therefore satisfies both public interfaces.
## Spring ownership
`MessagingBridgeRootAutoConfiguration` owns the bridge bean when an `EncodedMessagePublisher` is present **and** `app.messaging.producer-id` is explicitly configured. Producer identity is never inferred from `spring.application.name` or invented. Application bootstrap must not construct Kafka producer clients or implement broker-specific send behavior.
The existing `KafkaSender` / `KafkaMessageBroker` path remains temporarily for the legacy `OutboxEvent` and realtime publishers, which do not yet carry enough canonical metadata to enter the new bridge without invention. It is explicitly transitional and is removed only with the legacy outbox/realtime cutover. The new canonical bridge never calls it.
## Verification
Required checks:
1. `DefaultMessagePublisherTest`: pre-encoded publish preserves bytes and skips codec while still exercising central pipeline.
2. `PlatformIntegrationEventPublishAdapterTest`: golden mapping, outcome mapping, fail-closed identity behavior.
3. outbound messaging module tests/check.
4. messaging runtime/starter tests.
5. app-bootstrap system test and architecture test after adding the canonical bridge while retaining the documented legacy seam.
6. search proving app-bootstrap has no direct native Kafka sender configuration.
7. dependency/build lock refresh only where dependency ownership changed.
8. `git diff --check`.