# Messaging — First Principles ## 1. The Principles ### P1. Messages are Contracts A message has an explicit, versioned schema. Producer and consumer agree on shape before exchange; the schema is the boundary, not a guess. A schemaless message — a free-form JSON blob the consumer parses by hope — is a defect: the consumer breaks silently on the next shape change, and the producer has no contract to evolve against. This derives from `C1 Correctness` (the exchange must carry what the parties agreed to) and `C2 Clarity` (the schema makes the boundary obvious to both sides). This is the cross-process expression of the contract discipline that `domains/api/rest` owns for synchronous request/response: the message schema is to async exchange what the API contract is to sync exchange. It is distinct from `domains/concurrency/patterns` Pattern 1 (Message Passing), which owns the *in-process* channel primitive — here the contract spans separate systems and survives network failure (D-062). See `domains/messaging/queues.md` for the queue-flavored application and `domains/messaging/streams.md` for the durable-log-flavored application. ### P2. Ordering is a Property, Not an Assumption Ordering guarantees — per-partition strict, global, or none — are explicit and documented. "It's FIFO" is a claim that must be backed by the broker's partitioning contract, not an assumption the consumer makes and the broker may not honor. A standard queue delivers in arrival order per receive-node but offers no global ordering across shards; a FIFO queue delivers strict per-message- group order but at a latency cost; a partitioned stream delivers strict per-partition order but only within a partition. Each is a distinct, declared property. This derives from `C1 Correctness` (order is a correctness property — a consumer that assumes order the broker does not provide is wrong) and `C2 Clarity` (the ordering guarantee is documented, not discovered in production). This is distinct from in-process ordering, which `domains/concurrency/patterns` Pattern 1 owns for channels within one program: messaging ordering survives network failure, broker restart, and consumer crash-and-retry — a stronger failure model than thread-local channels (D-062). The `messaging-shared- subscription` chaos anti-pattern breaches this rule: two consumers sharing one subscription break per-consumer ordering because the broker dispatches each message to an arbitrary consumer. See `domains/messaging/streams.md` for the partition-order contract and `domains/messaging/delivery-semantics.md` for the interaction of ordering with the three delivery semantics. ### P3. Consumers are Idempotent Delivery is at-least-once by default across the network; a consumer deduplicates via idempotency keys or deterministic processing. "Exactly-once" is idempotency plus at-least-once, not a broker guarantee — Jepsen analyses of Kafka, RabbitMQ, and NATS establish that exactly-once claims require independent verification, and the durable engineering practice is to make consumers idempotent under redelivery. A non-idempotent consumer under at-least-once delivery doubles the effect on every retry; a non-idempotent consumer under a claimed exactly-once broker is a bug waiting for the broker's exactly-once invariant to break. This derives from `C1 Correctness`: correctness under redelivery is the contract, not a nice-to-have. This parallels `domains/edge/P5 Edge Operations are Idempotent` (the cross-partition device-and-cache-flavored analog) and is the cross-process instance of the retry-safety discipline that `domains/concurrency/patterns` Pattern 6 (Timeout on Every Block) implies for in-process retry. It is distinct from in-process retry because the redelivery comes from the broker across a network, not from an in-process loop (D-062). See `domains/messaging/delivery-semantics.md` for the idempotency-key dedup-store pattern. ### P4. Delivery Semantics are Explicit At-least-once / at-most-once / exactly-once is a declared choice per channel, not an emergent behavior. The tradeoff — latency cost, implementation complexity, operational cost — is made consciously and documented. At-most-once is fire-and-forget (low latency, lossy); at-least-once is acked with possible duplication (the default, requires idempotent consumers per P3); exactly-once is at-least-once plus idempotency or a transactional two-phase commit (highest cost, narrowest fit). An unstated semantic is a defect: the consumer guesses, and the guess is wrong under the first failure. This derives from `C1 Correctness` (the chosen semantic must hold) and `C2 Clarity` (the tradeoff is visible to the reader and the operator). This is the cross-process analog of the explicit-failure- mode discipline that `domains/errors/patterns` owns for synchronous code — messaging makes the delivery-mode choice as explicit as an error-handling choice. See `domains/messaging/queues.md` for the three-semantics comparison table and `domains/messaging/delivery- semantics.md` for the correctness properties of each. ### P5. Dead-Letter Handling is Defined Poison messages — unparseable, repeatedly failing, or exhausting the retry budget — are routed to a dead-letter queue, not retried forever or silently dropped. The DLQ is observable and drainable: an operator can inspect it, replay from it, or discard with audit. An unbounded retry loop is a livelock: the consumer never makes progress past the poison message. A silent drop is a correctness defect: the message vanished with no record. This derives from `C1 Correctness` (poison messages must not livelock the consumer or silently disappear) and `C5 Reversibility` (the DLQ is the reversibility mechanism — a dead-lettered message can be reprocessed after the bug is fixed). This is the cross-process analog of the bounded-error discipline that `domains/errors/patterns` owns for synchronous code: a poison message is an error-as-data instance that must be observable and recoverable, not swallowed. It is distinct from in-process error handling because the failure spans a network and a consumer restart (D-062). See `domains/messaging/delivery-semantics.md` for the dead-letter strategy comparison table and the DLQ routing rule pattern. ### P6. Backpressure is Bounded A slow consumer cannot unbounded-buffer the broker or the producer. Backpressure is explicit: consumer lag is visible, max-unacked is bounded, the retry budget is capped. A consumer that falls behind without a visible signal is a silent backlog — the operator cannot fix what they cannot see, and the broker's memory grows without bound until it fails. This derives from `C1 Correctness` (a backlog that grows until OOM is a correctness failure) and `C8 Economy` (the broker's memory is bounded by design, not by luck). This is distinct from `domains/concurrency/P9 Bounded Queues` and `domains/concurrency/patterns` Pattern 5 (Bounded Queue with Backpressure), which own the *in-process* analog: concurrency's bounded queue fails by OOM or thread crash; messaging's bounded backpressure fails by network partition, broker restart, or consumer crash-and-retry (D-062). The Reactive Streams specification (`request(n)`, `onNext` bounded) is the in-process instance; messaging's broker-backed backpressure is the cross-process instance above it. See `domains/messaging/queues.md` for prefetch and max-unacked and `domains/observability/metrics` for consumer-lag as an alert. ### P7. Partitioning is Intentional The partition key determines ordering, parallelism, and hotspots. Key choice is a design decision with documented rationale, not a default. A key that hashes unevenly creates a hot partition that limits throughput; a key that does not match the ordering need breaks per-key semantics; a key that is too coarse (one partition for the whole topic) serializes all the traffic. The partition count is a capacity bound: too few partitions cap parallelism, too many partition overhead the broker. This derives from `C4 Locality` (ordering and parallelism are co-located with the partition) and `C6 Composability` (the partition is the unit of parallelism and scaling — consumer groups compose from per-partition workers). This is the cross-process analog of the locality discipline that `domains/performance/` owns for generic data-near-compute optimization: performance's locality is algorithmic (data near compute); messaging's locality is partitional (order and parallelism near the partition). See `domains/messaging/streams.md` for the partitioned-log model and consumer-group rebalance strategies. ### P8. Replay and Retention are Configured Retention windows and replay-from-offset are explicit. A message is not ephemeral by default; the broker is a durable log, not a pipe. A topic with no retention is a fire-and-forget stream — a consumer that falls behind loses data permanently; a topic with infinite retention is an unbounded log — the broker grows until disk exhaustion. Both are defects: the retention window is a declared bound, and replay-from-offset is the mechanism that makes the log durable (re-consumable) rather than ephemeral. This derives from `C5 Reversibility` (a retained message is reversible — it can be re-consumed; an ephemeral message is not) and `C7 Observability` (the durable log is itself an observable record of what happened — the offset is the position from which to replay). This is the foundation for `domains/messaging/streams.md` and the rule that distinguishes a stream from a queue (a queue deletes on ack; a stream retains for replay). See `domains/messaging/pubsub.md` for the pub/sub-vs-stream durability boundary. ### P9. Schemas Evolve Compatibly Schema changes are backward- and forward-compatible by construction. Breaking changes are versioned migrations, not silent shape edits. A producer that ships a new field the old consumer ignores is backward-compatible; a consumer that handles a missing field the new producer omits is forward-compatible. A silent schema change — the producer renames a field and the consumer parses `undefined` — is a P1 violation (the contract was broken) compounded here as an evolution defect. This derives from `C5 Reversibility` (a schema change is reversible by versioning — the old shape is still readable) and `C6 Composability` (producers and consumers of different versions compose because the schema evolves compatibly). This parallels `domains/data/migrations` (schema migration for databases) and `domains/api/versioning` (API contract evolution): messaging's schema evolution is the async instance of the same compatibility discipline. See `domains/messaging/streams.md` for the stream-schema-evolution angle. ### P10. Messaging is Observable Consumer lag, DLQ depth, throughput, and consumer-group health are first-class signals. Silent backlog is a bug, not a feature: a consumer that falls behind with no lag metric is invisible until the downstream effect surfaces — by which time the backlog may be hours or days. A DLQ that grows without an alert is a silent correctness defect: poison messages are accumulating and no one knows. This derives from `C7 Observability` (the broker's behavior is visible to the operator) and `C1 Correctness` (backlog detection is a correctness bound — unbounded lag is a failure). This is distinct from `domains/observability/metrics`, which owns *generic* structured metrics; messaging owns the *broker-specific* signals — lag, DLQ depth, partition imbalance, consumer-group rebalance events. See `domains/observability/metrics` for the generic SLI/SLO discipline and `domains/observability/tracing` for cross-partition traces. ## 2. Core Principle Trace Each messaging P-rule derives from one or more core C-rules (C1–C8). The matrix extension lands in P4 of the v0.4 plan; the traces below are authoritative. Messaging is a broad-derivation domain touching 7 of 8 core principles (C1, C2, C4, C5, C6, C7, C8); C3 (Simplicity) is not a primary derivation — messaging is inherently a tradeoff domain where simplicity yields to the correctness of delivery guarantees (a simpler-than-necessary delivery model does not handle the failure cases, per C3's "simpler than necessary is also a violation"). | P-rule | Core | Why | |--------|------|-----| | P1 Messages are Contracts | C1, C2 | Correctness of the exchange; clarity of the schema boundary | | P2 Ordering is a Property, Not an Assumption | C1, C2 | Correctness of order; clarity of the guarantee | | P3 Consumers are Idempotent | C1 | Correctness under redelivery | | P4 Delivery Semantics are Explicit | C1, C2 | Correctness of the chosen semantic; clarity of the tradeoff | | P5 Dead-Letter Handling is Defined | C1, C5 | Correctness of poison-message routing; reversibility of reprocessing | | P6 Backpressure is Bounded | C1, C8 | Correctness of bounded backlog; economy of broker memory | | P7 Partitioning is Intentional | C4, C6 | Locality of order; composability of parallelism | | P8 Replay and Retention are Configured | C5, C7 | Reversibility of replay; observability of the durable log | | P9 Schemas Evolve Compatibly | C5, C6 | Reversibility of schema changes; composability of versions | | P10 Messaging is Observable | C7, C1 | Observability of lag/DLQ; correctness of backlog detection | ## 3. What Violates These Principles | Violation | Principle Breached | |-----------|-------------------| | Schemaless message (no versioned contract; consumer parses by guess) | P1 Messages are Contracts | | "It's FIFO" with no documented partition contract | P2 Ordering is a Property, Not an Assumption | | Non-idempotent consumer under at-least-once delivery | P3 Consumers are Idempotent | | Unstated delivery semantic (at-least-once vs exactly-once guessed) | P4 Delivery Semantics are Explicit | | No dead-letter queue (poison message retried forever or silently dropped) | P5 Dead-Letter Handling is Defined | | Unbounded retry budget (no cap; slow consumer stalls the partition) | P6 Backpressure is Bounded | | Default partition key (no rationale; hotspot or wrong-order) | P7 Partitioning is Intentional | | Ephemeral broker (no retention; no replay) | P8 Replay and Retention are Configured | | Silent schema change (producer breaks consumers with no version bump) | P9 Schemas Evolve Compatibly | | Silent backlog (no lag metric; consumer falls behind invisibly) | P10 Messaging is Observable | | Shared subscription (two consumers share one subscription; per-consumer ordering breaks) | P2 Ordering is a Property, Not an Assumption (P3 compounding) | | Blocking consumer (slow downstream call with no timeout; broker redelivers to the stuck consumer) | P6 Backpressure is Bounded | ## 4. Relationship to Other Domains Messaging systems are the engineering discipline of **cross-process, cross-system asynchronous communication via brokers**. Producer and consumer are separate systems; the broker is the intermediary that brokers delivery, ordering, retention, and failure semantics. The distinguishing constraints are a cross-process failure model (network, not crash), explicit delivery semantics, decoupled producer/consumer lifecycle, and replay-and-retention as a durable-log property. Messaging overlaps `domains/concurrency/` by *subject* (messages, queues, backpressure) but not by *failure model*: per D-062, messaging owns the cross-process/network-failure-model angle; concurrency owns the in-process/crash-failure-model angle. The discriminator is the failure model: concurrency's queue fails by OOM or thread crash; messaging's queue fails by network partition, broker restart, or consumer crash-and-retry. Messaging extends concurrency's bounded-queue/backpressure model to the network- partition regime. Cross-links are one-directional outward (per D-026 extended); no back-link edits to v0.1/v0.2/v0.3 content. - `domains/concurrency/patterns` ← P6 (the broker-backed bounded queue is the cross-process analog of the in-process bounded buffer — concurrency Pattern 5 owns in-process; messaging owns the network-failure-model instance above it, per D-062) - `domains/concurrency/patterns` ← P3 (idempotent retry is the cross-process analog of in-process retry-safety — the failure model differs: broker redelivery across a network vs in-process loop) - `domains/observability/metrics` ← P10 (consumer lag and DLQ depth as alerts; observability owns the generic SLI/SLO discipline, messaging owns the broker-specific signals) - `domains/observability/tracing` ← P10 (cross-partition traces for stream processing; observability owns the generic tracing discipline, messaging owns the cross-partition propagation) - `domains/data/schema-design` ← P1, P9 (message schema design and evolution; data owns the generic schema discipline, messaging owns the cross-process message-shape instance) - `domains/errors/patterns` ← P5 (errors as data for message failures; a poison message is an error-as-data instance that must be observable and recoverable, not swallowed) - `domains/edge/iot` ← P4 (the edge↔messaging cross-link resolves bidirectionally here: edge/iot.md links outward to messaging/queues for MQTT QoS parallels to delivery semantics; this first-principles doc acknowledges the back-link — the edge/iot.md → messaging/queues link from P1 now resolves because messaging/queues.md exists, completing the bidirectionality per IDEATE-40) > Note: the edge/iot.md → messaging/queues cross-link (MQTT QoS > parallels for delivery semantics) was authored in P1 with a > dangling reference; this P2 authorship of messaging/queues.md > resolves it. The bidirectionality is verified in P5 > (ATELIER-114 per IDEATE-40). The cross-link is one-directional > outward from edge/iot.md; this first-principles doc > acknowledges the resolution without editing edge/iot.md (per > D-026 extended — no back-link edits to v0.1/v0.2/v0.3 or to > P1-authored edge content).