PRD-022: Reliable Cross-Context Messaging Platform
Status: Planned
Version: 1.0
Date: 2025-12-28
Priority: High
Satisfies: ADR-032 (NATS JetStream)
Overview
- Feature: Reliable messaging between SEA™ and VibesPro™ bounded contexts
- Business Value: Ensures data consistency across contexts without distributed transactions
- Target Users: SEA™ platform developers, ops teams
Requirements
REQ-050: Transactional Outbox
| Field |
Value |
| REQ-ID |
REQ-050 |
| Type |
functional |
| EARS |
When a bounded context commits a state change, the system SHALL atomically insert an outbox event in the same transaction. |
| Bounded-Context |
sea, vibespro |
| Data Touched |
outbox_events table |
| Invariants |
INV-022 |
Acceptance Criteria:
1
2
3
4
5
| Given a command that modifies domain state
When the command handler commits
Then the state change and outbox event are in the same transaction
And if the transaction fails, neither is persisted
And INV-022 (atomic state+event) holds
|
Idempotency Requirement:
| Aspect | Specification |
|——–|—————|
| Repeatable? | yes |
| Safe to retry? | yes |
| Dedup strategy | outbox event UUID |
REQ-051: Inbox Idempotency
| Field |
Value |
| REQ-ID |
REQ-051 |
| Type |
functional |
| EARS |
When an event is received, the system SHALL record it in inbox before processing to ensure exactly-once semantics. |
| Bounded-Context |
sea, vibespro |
| Data Touched |
inbox_messages table |
| Invariants |
INV-021 |
Acceptance Criteria:
1
2
3
4
5
| Given an incoming event message
When the consumer attempts to process
Then it first inserts into inbox_messages with message_id PK
And if conflict (duplicate), processing is skipped
And INV-021 (exactly-once processing) holds
|
Idempotency Requirement:
| Aspect | Specification |
|——–|—————|
| Repeatable? | yes |
| Safe to retry? | yes |
| Dedup strategy | inbox PK (message_id) |
REQ-052: JetStream Redelivery
| Field |
Value |
| REQ-ID |
REQ-052 |
| Type |
functional |
| EARS |
When processing fails transiently, the system SHALL allow JetStream to redeliver the message. |
| Bounded-Context |
sea, vibespro |
| Data Touched |
N/A |
| Invariants |
INV-020 |
Acceptance Criteria:
1
2
3
4
5
6
| Given a message being processed
When the handler returns 5xx or times out
Then the consumer does NOT ACK the message
And JetStream redelivers after ack_wait expires
And the message appears again for processing
And INV-020 (at-least-once delivery) holds
|
Idempotency Requirement:
| Aspect | Specification |
|——–|—————|
| Repeatable? | yes |
| Safe to retry? | yes |
| Dedup strategy | JetStream + inbox |
REQ-053: Dead Letter Queue
| Field |
Value |
| REQ-ID |
REQ-053 |
| Type |
functional |
| EARS |
When processing fails as poison (422), the system SHALL route to DLQ and ACK the original. |
| Bounded-Context |
sea, vibespro |
| Data Touched |
DLQ stream |
| Invariants |
POL-022 |
Acceptance Criteria:
1
2
3
4
5
6
| Given a message with invalid payload or schema mismatch
When the handler returns 422 Unprocessable
Then the consumer publishes to DLQ subject
And marks original message as failed in inbox
And ACKs the original message (prevents infinite retry)
And POL-022 (poison handling) holds
|
Idempotency Requirement:
| Aspect | Specification |
|——–|—————|
| Repeatable? | yes |
| Safe to retry? | yes |
| Dedup strategy | DLQ message ID |
REQ-054: Publisher Deduplication
| Field |
Value |
| REQ-ID |
REQ-054 |
| Type |
functional |
| EARS |
When publishing to JetStream, the system SHALL set Nats-Msg-Id header for deduplication. |
| Bounded-Context |
sea, vibespro |
| Data Touched |
JetStream stream |
| Invariants |
INV-023 |
Acceptance Criteria:
1
2
3
4
5
| Given an outbox event to publish
When the publisher sends to JetStream
Then the Nats-Msg-Id header is set to outbox.id (UUID)
And JetStream deduplicates within its window
And republishes after crash don't create duplicates
|
Idempotency Requirement:
| Aspect | Specification |
|——–|—————|
| Repeatable? | yes |
| Safe to retry? | yes |
| Dedup strategy | Nats-Msg-Id header |
REQ-055: Per-Context Streams
| Field |
Value |
| REQ-ID |
REQ-055 |
| Type |
constraint |
| EARS |
Each bounded context SHALL own its own JetStream stream with clear subject namespace. |
| Bounded-Context |
sea, vibespro |
| Data Touched |
JetStream streams |
| Invariants |
N/A |
Acceptance Criteria:
1
2
3
4
5
| Given SEA™ and VibesPro™ contexts
When configuring JetStream
Then SEA_EVENTS stream captures sea.event.>
And VIBESPRO_EVENTS stream captures vibespro.event.>
And each context owns retention/limits for its stream
|
Idempotency Requirement:
| Aspect | Specification |
|——–|—————|
| Repeatable? | yes |
| Safe to retry? | yes |
| Dedup strategy | N/A |
REQ-056: Pull-Based Consumers
| Field |
Value |
| REQ-ID |
REQ-056 |
| Type |
constraint |
| EARS |
Consumers SHALL use pull-based subscription with explicit ACKs for backpressure control. |
| Bounded-Context |
sea, vibespro |
| Data Touched |
Consumer state |
| Invariants |
N/A |
Acceptance Criteria:
1
2
3
4
5
| Given a Rust consumer worker
When fetching messages from JetStream
Then it uses pull-based fetch with batch size
And explicitly ACKs after successful processing
And can control concurrency via max_ack_pending
|
Idempotency Requirement:
| Aspect | Specification |
|——–|—————|
| Repeatable? | yes |
| Safe to retry? | yes |
| Dedup strategy | N/A |
REQ-057: Event Versioning
| Field |
Value |
| REQ-ID |
REQ-057 |
| Type |
constraint |
| EARS |
Event subjects SHALL include version suffix and payload meaning MUST NOT change for a given version. |
| Bounded-Context |
All |
| Data Touched |
Event schemas |
| Invariants |
INV-023 |
Acceptance Criteria:
1
2
3
4
5
6
| Given an event type vibe_created
When publishing
Then subject is vibespro.event.vibe_created.v1
And .v1 payload schema is immutable
And breaking changes require .v2
And consumers can handle multiple versions
|
Idempotency Requirement:
| Aspect | Specification |
|——–|—————|
| Repeatable? | yes |
| Safe to retry? | yes |
| Dedup strategy | N/A |
REQ-058: Handler Envelope Contract
| Field |
Value |
| REQ-ID |
REQ-058 |
| Type |
functional |
| EARS |
When dispatching to a handler, the system SHALL send a full event envelope matching SDS-047 contract. |
| Bounded-Context |
sea, vibespro |
| Data Touched |
HTTP handler payload |
| Invariants |
POL-047-005, POL-047-006, POL-047-007 |
Acceptance Criteria:
1
2
3
4
5
| Given an event consumed from JetStream
When the worker dispatches to a handler
Then the HTTP request body includes message_id, subject, event_type, event_version,
And occurred_at, correlation_id, causation_id, aggregate_type, aggregate_id, payload
And the handler responds with 200/409/422/5xx per SDS-047
|
REQ-059: Dead Letter Replay
| Field |
Value |
| REQ-ID |
REQ-059 |
| Type |
functional |
| EARS |
When a message is routed to DLQ, the system SHALL attempt automated replay with exponential backoff and jitter. |
| Bounded-Context |
sea, vibespro |
| Data Touched |
DLQ stream, inbox_messages.dlq_metadata |
| Invariants |
INV-020 |
Acceptance Criteria:
1
2
3
4
5
| Given a message has been moved to DLQ
When the replay worker runs
Then it re-publishes after exponential delays with jitter
And updates replay_attempts in dlq_metadata
And stops replay after a configured max attempt cap
|
REQ-060: Handler Registry Generation
| Field |
Value |
| REQ-ID |
REQ-060 |
| Type |
constraint |
| EARS |
The handler registry SHALL be generated from manifests and require rebuild to apply handler changes. |
| Bounded-Context |
sea, vibespro |
| Data Touched |
Generated handler registry |
| Invariants |
N/A |
Acceptance Criteria:
1
2
3
4
| Given updated manifest events
When the registry generator runs
Then a static handler registry is produced with stable ordering
And the worker uses the generated registry without hot-reload
|
Success Metrics
| KPI |
Target |
Measurement Method |
| Message delivery rate |
99.99% |
outbox published / total |
| Duplicate processing rate |
0% |
inbox conflicts / total |
| DLQ rate |
< 0.1% |
DLQ count / total |
| End-to-end latency |
p99 < 500ms |
event timestamp diff |
Dependencies
- Depends on: ADR-032
- Blocks: SDS-047
- ADR: ADR-032, ADR-033
- SDS: SDS-047