Files
atelier/domains/messaging/first-principles.md
T
Jon Chery f61dffbb5a docs(P02): complete messaging domain phase — v0.4
---ci---
project: atelier
phase: 2
milestone: v0.4
status: complete
phase_role: execution
phase_tag: v0.3.2
requirements:
  covered: [ATELIER-97, ATELIER-98, ATELIER-99, ATELIER-100, ATELIER-101]
  partial: []
---/ci---
2026-08-05 16:00:31 +00:00

18 KiB
Raw Blame History

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 (C1C8). 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).