Files
clean-architecture-backend-…/docs/superpowers/plans/2026-09-18-outbox-transport-only-cutover.md
T

5.4 KiB

Outbox Transport-Only Cutover Implementation Plan

Goal: Preserve canonical integration-event bytes and metadata inside the existing legacy outbox_event authority, then route canonical claimed rows through the messaging platform while legacy rows keep the current broker path.

Spec: docs/superpowers/specs/2026-09-18-outbox-transport-only-cutover-design.md

Constraints

  • Preserve all existing uncommitted work. No reset/checkout/stage/commit/amend/push.
  • Do not activate or switch to POLLING_V2.
  • Do not synthesize missing canonical metadata for legacy rows.
  • Do not re-encode a persisted canonical envelope.
  • One claimed row goes through exactly one publish branch.
  • Default configuration remains legacy-compatible and canonical transport is off.

Task 1 — Split canonical and legacy append ports

  • Create LegacyOutboxAppendPort with the current NewOutboxEvent signature.
  • Change OutboxAppendPort to accept ValidatedIntegrationEvent.
  • Move all current raw production consumers and their tests to LegacyOutboxAppendPort.
  • Make OutboxStoreAdapter implement LegacyOutboxAppendPort only.
  • Run :application-core:test and focused sample/outbox compile tests.

Task 2 — Add additive canonical columns to outbox_event

  • Add the next PostgreSQL migration after V12.
  • Widen event/correlation identifiers as required.
  • Add canonical metadata, exact BYTEA, hashes/revisions, and all-or-none check constraints.
  • Extend OutboxEventEntity mappings.
  • Update migration history expectations.
  • Add real PostgreSQL integration assertions for legacy rows and canonical shape constraints.

Task 3 — Implement canonical append adapter

  • Add CanonicalOutboxAppendAdapter.
  • Strictly validate UTF-8 compatibility projection.
  • Persist every canonical field and exact envelopeBytes.
  • Preserve old required columns for legacy relay/storage compatibility.
  • Add unit tests for byte equality, field mapping and invalid UTF-8.
  • Gate bean exposure on ca-skeleton.outbox.canonical-transport-enabled=true.

Task 4 — Split the claimed row model

  • Add sealed ClaimedOutboxEvent.
  • Keep OutboxEvent as legacy subtype.
  • Add CanonicalClaimedOutboxEvent carrying reconstructed ValidatedIntegrationEvent.
  • Change OutboxStorePort.claimBatch to return the sealed type.
  • Map all-canonical rows to canonical subtype and all-null rows to legacy subtype.
  • Reject partial canonical rows.
  • Update legacy relay tests without changing its state-machine semantics.

Task 5 — Route canonical claims through the platform

  • Update OutboxMessagePublishPort to accept ClaimedOutboxEvent.
  • Extend OutboxMessagePublishAdapter with canonical IntegrationEventPublishPort.
  • Legacy subtype uses only MessageBroker.
  • Canonical subtype uses only the application canonical publish port and exact stored bytes.
  • Add focused branch-isolation and outcome tests.

Task 6 — Add explicit activation and composition validation

  • Add canonicalTransportEnabled to OutboxSettings and config/outbox.yml, default false.
  • Startup fails when canonical transport is enabled but no IntegrationEventPublishPort exists.
  • Relay-enabled compatibility deployment still requires the legacy broker until a later zero-legacy-backlog proof.
  • Default-off composition keeps the existing legacy path.
  • Enabled composition exposes the canonical append/publish path without a second scheduler.
  • Update configuration docs/SSOT.

Task 7 — Regression and architecture verification

  • :application-core:check
  • :adapter:outbound:persistence-jpa:check
  • focused PostgreSQL migration/outbox integration lane
  • :adapter:outbound:messaging:check
  • :sample-portfolio:check — blocked by pre-existing JpaLiveEventReplayAdapter missing Duration wiring; the same 3 SampleApplicationContextTest failures reproduce on clean HEAD.
  • focused sample outbox regression tests (PosterEventPublisherTest, CreateWorkLogOutboxTest, WorkLogUseCasesTest, WorkLogAuthorizationContractTest)
  • :app-bootstrap:architectureTest :app-bootstrap:systemTest
  • verifyCleanArchitectureDependencies
  • :app-bootstrap:verifyEnvKeys
  • static scans: no platform runtime/Kafka import in canonical bridge
  • git diff --check
  • no separate LLM Wiki branch-note warranted; spec, plan, module README/CLAUDE and configuration reference carry the implementation decision.

Verification result

Transport-only cutover implementation is complete for this slice.

Passed:

  • :application-core:check
  • :adapter:outbound:persistence-jpa:check
  • :adapter:outbound:persistence-jpa:postgresqlMigrationIntegrationTest
  • :adapter:outbound:messaging:check
  • focused sample outbox regression tests
  • :app-bootstrap:test
  • focused :app-bootstrap:integrationTest outbox append + row-lifecycle contracts
  • :app-bootstrap:architectureTest
  • :app-bootstrap:systemTest
  • verifyCleanArchitectureDependencies
  • :app-bootstrap:verifyEnvKeys
  • git diff --check

Known unrelated blocker:

  • full :sample-portfolio:check still fails only the 3 previously documented SampleApplicationContextTest cases because JpaLiveEventReplayAdapter requires an unbound Duration bean. This reproduces on clean HEAD and was not introduced by this cutover.

Publication authority remains LEGACY_POLLING; no code path in this slice activates POLLING_V2.