How to Read This Post
Each scenario shows a diagram first, then a short note on why that system fits best. Complexity increases as you scroll.
| System | Strength |
|---|---|
| RabbitMQ | Smart broker, routing flexibility, push-based delivery |
| Kafka | Immutable log, high throughput, replay from any offset |
| JetStream NATS | Lightweight persistence, built-in dedup, low operational cost |
Level 1 — Foundations
1. Simple Task Queue
RabbitMQ. Push-based delivery with acknowledgments. Consumer confirms completion before the broker removes the message. No polling, no offset tracking — just send and ack.
2. Broadcast Fan-out
RabbitMQ. Fanout exchange copies every message to all bound queues. Each downstream service gets its own independent queue with separate ack tracking.
3. Fire-and-Forget Pub/Sub
NATS Core. Sub-millisecond latency, no broker-side persistence, zero configuration. If a subscriber is offline, the message is gone. Perfect for telemetry, heartbeats, ephemeral events where loss is acceptable.
Level 2 — Distribution
4. Competing Consumers (Fair Dispatch)
RabbitMQ. Round-robin dispatch across workers. prefetch=1 ensures a slow worker doesn’t hoard messages — the broker only sends the next task when the previous ack arrives.
5. Topic-Based Routing
RabbitMQ. Topic exchange with wildcard patterns (* matches one word, # matches zero or more). Routing logic lives in the broker — producers don’t need to know who’s listening.
6. Event Log with Replay
───────
offset 0 → 1 → 2 → 3 → 4 → 5"] P_1["Partition 1
───────
offset 0 → 1 → 2 → 3 → 4 → 5"] P_2["Partition 2
───────
offset 0 → 1 → 2 → 3 → 4 → 5"] end T --> CG1["Consumer Group A
📍 offset: 5 (live)"] T --> CG2["Consumer Group B
📍 offset: 2 (catching up)"] T --> CG3["New Consumer
📍 offset: 0 (full replay)"] style P1 fill:#4a9eff,stroke:#2d7ed8,color:#fff style P2 fill:#4a9eff,stroke:#2d7ed8,color:#fff style T fill:#1e3a8a,stroke:#e94560,color:#fff style P0 fill:#546e7a,stroke:#90a4ae,color:#fff style P_1 fill:#546e7a,stroke:#90a4ae,color:#fff style P_2 fill:#546e7a,stroke:#90a4ae,color:#fff style CG1 fill:#51cf66,stroke:#37b24d,color:#fff style CG2 fill:#ffd43b,stroke:#f59f00,color:#333 style CG3 fill:#ff6b6b,stroke:#d44,color:#fff
Kafka. The log is immutable — messages aren’t deleted on read. Each consumer group tracks its own offset. A new service can replay from offset 0 to rebuild its entire state. This is impossible with RabbitMQ’s destructive reads.
Level 3 — Reliability
7. Ordered Processing at Scale
key = customer-7"] O2["Order: customer-3
key = customer-3"] O3["Order: customer-7
key = customer-7"] end subgraph Kafka["Kafka Topic: orders"] direction LR PA["Partition 0
───────
customer-3 events
(hash mod 3 = 0)"] PB["Partition 1
───────
customer-7 events
(hash mod 3 = 1)"] PC["Partition 2
───────
(other customers)"] end O1 -->|"hash(key)"| PB O2 -->|"hash(key)"| PA O3 -->|"hash(key)"| PB subgraph CG["Consumer Group"] C1["Consumer 1 ↔ P0"] C2["Consumer 2 ↔ P1"] C3["Consumer 3 ↔ P2"] end PA --> C1 PB --> C2 PC --> C3 style O1 fill:#4a9eff,stroke:#2d7ed8,color:#fff style O2 fill:#4a9eff,stroke:#2d7ed8,color:#fff style O3 fill:#4a9eff,stroke:#2d7ed8,color:#fff style PA fill:#546e7a,stroke:#90a4ae,color:#fff style PB fill:#546e7a,stroke:#90a4ae,color:#fff style PC fill:#546e7a,stroke:#90a4ae,color:#fff style C1 fill:#51cf66,stroke:#37b24d,color:#fff style C2 fill:#51cf66,stroke:#37b24d,color:#fff style C3 fill:#51cf66,stroke:#37b24d,color:#fff
Kafka. Same key always maps to same partition. All events for customer-7 are processed in order by a single consumer. You scale horizontally by adding partitions — ordering is preserved per key, parallelism per partition.
8. Offline Consumer / Catch-Up
unacked messages Note over C: Consumer reconnects 🔄 JS->>C: deliver(msg 1) C->>JS: ack JS->>C: deliver(msg 2) C->>JS: ack JS->>C: deliver(msg 3) C->>JS: ack JS->>C: deliver(msg 4) C->>JS: ack JS->>C: deliver(msg 5) C->>JS: ack
JetStream NATS. Adds persistence on top of NATS Core. Streams retain messages; durable consumers resume from their last ack. Lighter than Kafka’s operational footprint — no ZooKeeper, no JVM, single binary.
9. Dead Letter Queue + TTL Retry
RabbitMQ. Dead Letter Exchanges (DLX) are a first-class feature. Failed messages route to a DLQ automatically. A wait queue with TTL creates retry loops — no external scheduler, no cron jobs. Kafka requires you to build all of this yourself.
Level 4 — Distributed Patterns
10. CQRS + Event Sourcing
(source of truth)"] KL --> PR1["Projector: SQL DB
📍 offset 1204"] KL --> PR2["Projector: Search Index
📍 offset 1198"] KL --> PR3["Projector: Analytics
📍 offset 1150"] PR1 --> DB1[(PostgreSQL)] PR2 --> DB2[(Elasticsearch)] PR3 --> DB3[(ClickHouse)] QR["Query API"] -->|"read"| DB1 QR -->|"search"| DB2 QR -->|"dashboard"| DB3 style CMD fill:#4a9eff,stroke:#2d7ed8,color:#fff style WS fill:#4a9eff,stroke:#2d7ed8,color:#fff style KL fill:#e94560,stroke:#c0392b,color:#fff style PR1 fill:#ffd43b,stroke:#f59f00,color:#333 style PR2 fill:#ffd43b,stroke:#f59f00,color:#333 style PR3 fill:#ffd43b,stroke:#f59f00,color:#333 style DB1 fill:#51cf66,stroke:#37b24d,color:#fff style DB2 fill:#51cf66,stroke:#37b24d,color:#fff style DB3 fill:#51cf66,stroke:#37b24d,color:#fff style QR fill:#a29bfe,stroke:#6c5ce7,color:#fff
Kafka. The event log IS the source of truth — not the database. Each projector consumes the same stream at its own pace to build a specialized read model. Need a new view? Deploy a new consumer, replay from offset 0. Existing projectors are unaffected.
11. Saga Choreography
Kafka. No central orchestrator. Each service listens for events it cares about, does its work, emits the next event. Failures trigger compensating events that flow backwards. The event log provides a complete audit trail of every state transition.
12. Stream Processing Pipeline
(clickstream)"] --> T1["Topic:
raw-clicks"] T1 --> F["Filter &
Enrich"] F --> T2["Topic:
enriched-clicks"] T2 --> AGG["Windowed
Aggregation
(5 min tumbling)"] AGG --> T3["Topic:
click-counts"] T3 --> SINK1["Dashboard"] T3 --> SINK2["Alert Engine"] subgraph KS["Kafka Streams Application"] F AGG end style SRC fill:#4a9eff,stroke:#2d7ed8,color:#fff style T1 fill:#546e7a,stroke:#90a4ae,color:#fff style T2 fill:#546e7a,stroke:#90a4ae,color:#fff style T3 fill:#546e7a,stroke:#90a4ae,color:#fff style F fill:#ffd43b,stroke:#f59f00,color:#333 style AGG fill:#ffd43b,stroke:#f59f00,color:#333 style SINK1 fill:#51cf66,stroke:#37b24d,color:#fff style SINK2 fill:#51cf66,stroke:#37b24d,color:#fff style KS fill:#1e3a8a,stroke:#e94560,color:#fff
Kafka Streams. Processing runs inside your application — no separate cluster (unlike Flink/Spark). Intermediate results go back to Kafka topics. Stateful operations (windowed counts, joins) use local RocksDB stores backed by changelog topics for fault tolerance. Exactly-once semantics built in.
Level 5 — Expert
13. Multi-Region Replication
async replication
offset translation"| K2 subgraph R3["Region: AP-Southeast"] K3["Kafka Cluster
(read replica)"] K3 --> C3[Local Consumers] end K1 -->|"MirrorMaker 2
one-way"| K3 style P1 fill:#4a9eff,stroke:#2d7ed8,color:#fff style P2 fill:#4a9eff,stroke:#2d7ed8,color:#fff style K1 fill:#e94560,stroke:#c0392b,color:#fff style K2 fill:#e94560,stroke:#c0392b,color:#fff style K3 fill:#78909c,stroke:#546e7a,color:#fff style C1 fill:#51cf66,stroke:#37b24d,color:#fff style C2 fill:#51cf66,stroke:#37b24d,color:#fff style C3 fill:#51cf66,stroke:#37b24d,color:#fff
Kafka. MirrorMaker 2 replicates topics across datacenters with automatic offset translation. US-East ↔ EU-West runs active-active (both produce and consume locally). AP-Southeast runs as a read replica. Consumers see local latency regardless of where the event originated.
14. Exactly-Once with Deduplication
producer retries ⚡ P->>JS: publish(msg, Nats-Msg-Id: "order-123-v1") JS-->>P: ack (duplicate, ignored) Note over JS: Dedup window: 2 min
Only one copy stored JS->>C: deliver(msg) C->>JS: ack Note over JS: Double-ack protection:
AckWait + MaxDeliver Note over C: Consumer processes
exactly once ✓
JetStream NATS. Built-in message deduplication using Nats-Msg-Id headers with a configurable time window. Producer retries are safe — duplicates are detected and discarded at the broker. Combined with MaxDeliver and AckWait on the consumer side, you get end-to-end exactly-once without idempotency logic in your application.
15. Hybrid Architecture: Right Tool Per Layer
< 1ms"| NATS((NATS)) subgraph Services["Microservices"] S1[Auth Service] S2[User Service] S3[Product Service] end NATS <--> Services Services -->|"domain events"| KAFKA((Kafka)) KAFKA --> STREAM["Stream Processing
(Kafka Streams)"] KAFKA --> ES["Event Store
(replay / audit)"] KAFKA --> PROJ["CQRS Projectors"] Services -->|"background jobs"| RMQ((RabbitMQ)) RMQ --> W1["Email Worker"] RMQ --> W2["PDF Generator"] RMQ --> W3["Image Resizer"] style CLIENT fill:#a29bfe,stroke:#6c5ce7,color:#fff style API fill:#a29bfe,stroke:#6c5ce7,color:#fff style NATS fill:#27ae60,stroke:#1e8449,color:#fff style S1 fill:#4a9eff,stroke:#2d7ed8,color:#fff style S2 fill:#4a9eff,stroke:#2d7ed8,color:#fff style S3 fill:#4a9eff,stroke:#2d7ed8,color:#fff style KAFKA fill:#e94560,stroke:#c0392b,color:#fff style STREAM fill:#ffd43b,stroke:#f59f00,color:#333 style ES fill:#ffd43b,stroke:#f59f00,color:#333 style PROJ fill:#ffd43b,stroke:#f59f00,color:#333 style RMQ fill:#ff6b6b,stroke:#d44,color:#fff style W1 fill:#51cf66,stroke:#37b24d,color:#fff style W2 fill:#51cf66,stroke:#37b24d,color:#fff style W3 fill:#51cf66,stroke:#37b24d,color:#fff
All three — each where it excels.
| Layer | System | Why |
|---|---|---|
| Service-to-service RPC | NATS | Sub-millisecond request-reply, no serialization overhead |
| Event log / streaming | Kafka | Immutable log, replay, stream processing, multi-consumer |
| Background jobs | RabbitMQ | Ack-based delivery, DLQ, priority queues, mature tooling |
No single messaging system wins every scenario. The right architecture uses each where its design assumptions match your requirements.
Decision Matrix
TL;DR
- Need smart routing, task queues, or retry logic? → RabbitMQ
- Need an event log, replay, or stream processing? → Kafka
- Need lightweight persistence without operational overhead? → JetStream NATS
- Need sub-millisecond fire-and-forget? → NATS Core
- Building a real system? → Probably more than one