f61dffbb5a
---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---
307 lines
18 KiB
Markdown
307 lines
18 KiB
Markdown
# 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). |