# Bad Example: Messaging Shared Subscription > Two consumers share one subscription; the broker dispatches each > message to an arbitrary consumer. Per-consumer ordering breaks; > per-consumer dedup is wrong. This is the pre-specified > `messaging-shared-subscription` chaos anti-pattern (ATELIER-110). > **Single-breach per D-068:** the primary principle breached is > **Messaging P2 (Ordering is a Property, Not an Assumption)**. P3 > (Consumers are Idempotent) is noted as the compounding > consequence — the example remains single-breach in its named > violation. ## The Code ```python # Two email workers share ONE subscription on the "users" topic. # The broker round-robins: worker A gets msg 1, msg 3; worker B # gets msg 2, msg 4. Per-consumer order is broken (P2 breach). # Each worker has its OWN dedup store — a redelivery to the other # worker re-processes (P3 compounding). import json def shared_subscription_bad(broker, send_email, worker_id: str): # P2 VIOLATION: both workers call subscribe with the SAME # subscription name. The broker dispatches each message to an # arbitrary worker in the shared group. Worker A sees msg 3 # before worker B sees msg 1; per-worker order is broken. sub = broker.subscribe(topic="users", subscription="welcome-shared") # P3 compounding: per-worker dedup. A redelivered message may # land on the OTHER worker, which has not seen it, so the # per-worker dedup store does not catch it — the message is # processed twice across the two workers. local_dedup = DedupStore(backend=redis_for(worker_id)) for message in sub.receive(): payload = json.loads(message["body"]) if local_dedup.seen(payload["idempotencyKey"]): sub.ack(message); continue send_email(payload["email"], "Welcome!") local_dedup.mark(payload["idempotencyKey"]) sub.ack(message) ``` ```python # The two workers are launched with the same subscription name. # Worker A and Worker B both call subscribe("users", "welcome-shared"). # The broker sees one shared subscription; it round-robins. def start_workers(broker): # Worker A spawn(shared_subscription_bad, broker, send_email, worker_id="A") # Worker B — same subscription name, same topic spawn(shared_subscription_bad, broker, send_email, worker_id="B") # The broker dispatches: A gets msg 1, B gets msg 2, A gets msg 3, # B gets msg 4. If worker A is slow, B may get msg 2 and msg 4 # before A acks msg 1. Per-worker order is broken (P2). ``` The scenario: a user signs up, then immediately updates their email preference (two messages in topic order: `user.signed-up.v1`, `user.preference-updated.v1`). The broker dispatches `signed-up` to worker A and `preference-updated` to worker B. Worker B sends the preference email before worker A sends the welcome email — the user sees the preference confirmation before the welcome. Then the broker redelivers `signed-up` (worker A's ack was slow); this time it dispatches to worker B. Worker B's local dedup store has never seen `signed-up` (it was processed by A), so B sends the welcome email again. The user receives two welcome emails. Per-consumer ordering broke (P2); per-consumer dedup did not catch the cross-worker redelivery (P3 compounding). ## Why It Violates ### Ordering is a Property, Not an Assumption (Messaging P2, C1, C2) - **The breach (primary):** the two workers share one subscription, and the broker dispatches each message to an arbitrary worker in the shared group. Per-consumer ordering is broken: worker A sees `preference-updated` (msg 2) before it sees `signed-up` (msg 1) if the broker round-robins them to different workers. The workers' code assumes topic order, but the shared subscription provides no per-consumer order guarantee — the broker's dispatch is arbitrary. P2 requires that the ordering property be explicit and documented; here it is assumed (topic order) but not provided (arbitrary dispatch). The assumption is wrong. - The P2 contract is that "it's FIFO" is a claim backed by the broker's partitioning contract, not an assumption the consumer makes. A shared subscription's contract is "no per-consumer order"; the workers' code assumes the opposite. See `domains/messaging/pubsub.md` (Shared vs Independent Subscriptions) and `domains/messaging/first-principles.md` P2. ### Consumers are Idempotent — compounding consequence (Messaging P3, C1) - **The compounding consequence (not the named breach):** each worker has its OWN dedup store. A redelivered message that lands on the *other* worker is not in that worker's dedup store, so it is processed again. The dedup is per-worker, but the subscription is shared — the dedup must be shared across workers to be correct under a shared subscription. The per-worker dedup store is wrong for a shared subscription; a shared dedup store (a shared Redis, a shared DB) is required. - Per D-068, the example remains single-breach in its named violation: P2 is the primary breach (the shared subscription breaks ordering); P3 is the compounding consequence (the per-worker dedup is wrong *because* the subscription is shared). If the subscription were independent, per-worker dedup would be correct. The shared subscription is the root cause; P3 is the downstream effect. ## The Fix ```python # Fix 1 (default): independent subscriptions. Each consumer gets # its own durable cursor; per-consumer order holds (P2); per- # consumer dedup is correct (P3). This is the default per # domains/messaging/pubsub.md. def independent_subscriptions_good(broker, send_email, worker_id: str): # Each worker has its OWN subscription name. The broker # delivers every message to every subscription in topic order. sub = broker.subscribe( topic="users", subscription=f"welcome-{worker_id}", # per-consumer ) # Per-consumer dedup is now correct: a redelivery to THIS # worker is caught by THIS worker's dedup store. dedup = DedupStore(backend=redis_for(worker_id)) for message in sub.receive(): payload = json.loads(message["body"]) if dedup.seen(payload["idempotencyKey"]): sub.ack(message); continue send_email(payload["email"], "Welcome!") dedup.mark(payload["idempotencyKey"]) sub.ack(message) ``` ```python # Fix 2 (if a shared subscription is genuinely required): the # consumers must be stateless, the processing order-independent, # AND the dedup store must be SHARED across workers. Document the # ordering property as "none across consumers" (P2 — the property # is explicit, not assumed) and use a shared dedup backend (P3). def shared_subscription_stateless(broker, send_email): # P2: document the ordering property. A shared subscription # provides NO per-consumer order; processing must be order- # independent. Do not assume topic order. sub = broker.subscribe(topic="users", subscription="welcome-shared") # P3: SHARED dedup. A redelivery to any worker is caught by the # shared store. shared_dedup = DedupStore(backend=shared_redis) for message in sub.receive(): payload = json.loads(message["body"]) if shared_dedup.seen(payload["idempotencyKey"]): sub.ack(message); continue send_email(payload["email"], "Welcome!") # order-independent shared_dedup.mark(payload["idempotencyKey"]) sub.ack(message) ``` The default is Fix 1 (independent subscriptions): per-consumer ordering holds, per-consumer dedup is correct, and the code is simpler. Fix 2 is the narrow opt-in for genuinely stateless, order-independent processing — and even then, the dedup must be shared. See `domains/messaging/pubsub.md` for the full shared-vs-independent discussion. ## Cross-Domain Links - `domains/messaging/pubsub.md` — the shared-vs-independent subscriptions section; the `messaging-shared-subscription` anti-pattern lives here (pre-specified in P4 ATELIER-110). - `domains/messaging/first-principles.md` — P2 (Ordering is a Property, Not an Assumption) is the primary breach; P3 (Consumers are Idempotent) is the compounding consequence. - `domains/messaging/delivery-semantics.md` — the idempotency-key dedup store; the per-subscription dedup key scoping (`(subscription, idempotencyKey)`) that prevents one subscription's dedup from masking another's redelivery. - `review/anti-patterns.md` — the `messaging-shared-subscription` chaos anti-pattern (messaging P2, P3, C1 — shared subscription breaks ordering and dedup). - `examples/good/messaging-idempotent-consumer.md` — the good- example version of the consumer pattern: an independent consumer with a dedup store and a DLQ routing rule.