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

4.8 KiB

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:

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.