Async Messaging & Queues
When to use
- Decoupling producers from consumers across service boundaries
- Guaranteeing at-least-once or exactly-once delivery
- Fan-out to multiple consumers (pub/sub)
- Rate-limiting downstream services (backpressure)
- Durable event log / event sourcing (Kafka)
- Job queues for background processing (SQS, RabbitMQ)
- Ordered processing of domain events
Workflow
- Clarify delivery contract — Ask: ordered or unordered? at-least-once or exactly-once? max latency? consumer count? message size p99? retention needed?
- Select broker — Use the decision matrix in Standards below.
- Design topic/queue schema — Name conventions, partition count (Kafka), routing keys (RabbitMQ), FIFO vs standard (SQS).
- Define message envelope — Header fields (event-type, correlation-id, causation-id, schema-version, produced-at ISO-8601). Payload in JSON or Avro/Protobuf.
- Implement producer — Transactional outbox if DB write + publish must be atomic. Otherwise wrap in try/catch with structured logging of failure.
- Implement consumer — Idempotency key stored in DB or Redis before processing. Ack only after successful processing + side-effects committed.
- Configure DLQ — Dead-letter after N retries (3–5). Alert on DLQ depth > 0. DLQ messages must retain original headers + failure reason.
- Backpressure — Consumer-side: bounded prefetch/max in-flight (RabbitMQ
prefetch_count, Kafkamax.poll.records). Producer-side: circuit breaker or local queue with shed policy. - Schema evolution — Backward/forward-compatible changes only (add optional fields, never remove or rename). Register schemas in Confluent Schema Registry or AWS Glue Schema Registry.
- Observability — Emit consumer lag, processing latency, error rate, DLQ depth. Kafka: use
kafka_consumer_group_lagvia JMX/MSK metrics. SQS:ApproximateNumberOfMessagesNotVisible,ApproximateAgeOfOldestMessage. - Security — mTLS or SASL/SCRAM for Kafka. IAM policies scoping producer/consumer per topic for SQS/SNS. Encryption at rest (KMS). No secrets in message payloads.
- Test — Unit-test handlers with in-memory stub. Integration-test with Testcontainers (kafka container
confluentinc/cp-kafka:7.x, localstack for SQS). Chaos: kill broker mid-test, assert idempotent re-delivery.
Standards
Broker selection matrix
| Requirement | Best choice | Avoid |
|---|---|---|
| Durable ordered log, replay, high throughput (>50k msg/s) | Kafka (MSK or self-hosted) | RabbitMQ (no log replay) |
| Simple job queue, SaaS, no ops overhead | SQS Standard or SQS FIFO | Kafka (overkill) |
| Complex routing (topic exchanges, fanout, per-message TTL) | RabbitMQ | SQS |
| Sub-millisecond latency, request-reply, lightweight IoT | NATS JetStream | Kafka |
| GCP-native | Cloud Pub/Sub | — |
| Azure-native | Azure Service Bus (queues/topics) or Event Hubs (Kafka-compat) | — |
| Exactly-once without outbox | Kafka transactions (transactional.id) |
SQS (no exactly-once) |
Kafka specifics (versions 3.x)
- Partition count: start at
max_consumers_expected × 2, never decrease. - Replication factor: 3 in prod,
min.insync.replicas=2. - Producer:
acks=all,enable.idempotence=true,compression.type=lz4. - Consumer:
enable.auto.commit=false; commit only after processing + downstream acks. - Exactly-once:
isolation.level=read_committed, use Kafka Streams or explicit transactions. - Retention: set per topic (
retention.ms,retention.bytes); default 7 days for audit topics. - Monitoring: Cruise Control for rebalancing; Kafka UI or Redpanda Console for dev.
SQS specifics
- Standard queue for throughput; FIFO queue (300 TPS, or 3000 with batching) for ordering.
VisibilityTimeout≥ 6× max processing time to prevent double-delivery.ReceiveMessageWaitTimeSeconds=20(long polling) to cut cost.- Batch receive (
MaxNumberOfMessages=10) in consumers. - DLQ: set
maxReceiveCount=3; redrive policy referencing DLQ ARN. - Large messages (>256 KB): use SQS Extended Client Library with S3.
RabbitMQ specifics (versions 3.12+)
- Use quorum queues (not classic mirrored) for HA.
prefetch_countper channel, not per connection.- Publisher confirms:
channel.confirm_select()+ wait for ack before returning success to caller. - Dead-lettering: set
x-dead-letter-exchange+x-dead-letter-routing-keyon queue declaration. - Use stream queues (3.9+) for log-like replay semantics.
Outbox pattern (transactional outbox)
Use when: producer must atomically write to DB and enqueue a message.
- Write domain record + outbox record in same DB transaction.
- Polling relay (or Debezium CDC) reads outbox table, publishes to broker, marks sent.
- Consumer deduplicates via
message_idin processed-events table (upsert on conflict do nothing).
- Debezium connectors:
debezium/debezium-connector-postgres:2.xfor Postgres → Kafka. - Polling relay: query
WHERE published_at IS NULL ORDER BY created_at LIMIT 100every 500 ms.
Idempotency
- Every consumer maintains an
idempotency_key → processed_atstore (Postgres unique index or Redis SET NX with TTL). - Key =
message_idfrom broker header (UUID v4 set by producer). Never use payload fields as key. - Window: keep keys for
max(retention_period, visibility_timeout × max_retries).
Ordering guarantees
- Kafka: order guaranteed within partition; use message key = aggregate ID.
- SQS FIFO: order within
MessageGroupId; set group = aggregate ID. - RabbitMQ: single consumer per queue for strict order (no competing consumers).
- Never assume cross-partition/cross-queue ordering.
Common mistakes to avoid
- Auto-commit before processing — loses messages on consumer crash; always commit after.
- No DLQ — poison messages block the queue forever.
- Visibility timeout too short (SQS) — causes duplicate delivery under load.
- Single partition (Kafka) — no parallelism; impossible to scale later without disruption.
- Putting secrets in message body — messages are often logged; use references instead.
- Synchronous HTTP in consumer — creates tight coupling; use async or circuit breaker.
- Ignoring consumer lag — queue depth spike is the earliest signal of a downstream incident.
- Breaking schema changes — removing/renaming fields breaks consumers silently; use schema registry + compatibility checks in CI.
- Unbounded retries without backoff — cascades into broker saturation; use exponential backoff with jitter (base 100 ms, max 30 s).
Output format
Produce artifacts in docs/architecture/messaging/ using template .claude/templates/architecture.md adapted for the messaging context:
broker-decision.md— rationale for broker choicetopic-schema.md— topic/queue names, partition counts, retention, routing keysmessage-envelope.md— field definitions with types and required/optionalconsumer-playbook.md— idempotency strategy, DLQ handling, retry policy, observability- Code scaffolds (producer + consumer) inline in the message-envelope doc as code blocks
Related checklists
.claude/checklists/architecture.md.claude/checklists/security.md.claude/checklists/observability.md.claude/checklists/backend.md
Related agents
.claude/agents/engineering/backend-engineer.md.claude/agents/engineering/data-engineer.md.claude/agents/engineering/realtime-engineer.md.claude/agents/engineering/integration-engineer.md.claude/agents/quality/reliability-engineer.md.claude/agents/core/solution-architect.md