DEV Community

Cover image for How Services Send Messages Without Losing Them (Explained with Swiggy)
Vignesh Athiappan
Vignesh Athiappan

Posted on

How Services Send Messages Without Losing Them (Explained with Swiggy)

Instead of calling each other directly, services often drop messages and move on. It's more resilient — but it raises real questions. What if the receiver is down? What if a message gets processed twice and a customer is charged double? These 5 patterns are the answers.


The 5 patterns

# Pattern One-line
1 Message Queue A buffer between sender and receiver
2 Publish-Subscribe One message, many independent receivers
3 Competing Consumers Many workers share one queue to scale
4 Dead-Letter Queue Where failing messages go to be inspected
5 Idempotent Consumer Processing twice = same result as once

1. Message Queue

A buffer that holds messages between sender and receiver. One message → one consumer.

Order svc → [ ▪▪▪ QUEUE ▪▪▪ ] → Payment svc
            (holds messages safely)
Enter fullscreen mode Exit fullscreen mode
✅ Good ❌ Bad
Sender & receiver decoupled Adds a component to manage
Receiver can be down — the message waits Not instant (it's async)
Absorbs traffic spikes Ordering can be tricky

Swiggy: "charge this order" is dropped on a queue. If Payment is briefly down, the message waits — nothing is lost.
Azure: Service Bus Queue.
Key idea: each message is consumed once, by one worker.


2. Publish-Subscribe (Pub-Sub)

One message published → delivered to MANY subscribers, each independently.

The difference from a queue: a queue delivers to one. Pub-sub delivers a copy to everyone who subscribed.

"Order placed" → [ TOPIC ] →→ Payment    (own copy)
                            →→ Restaurant (own copy)
                            →→ Analytics  (own copy)
Enter fullscreen mode Exit fullscreen mode
✅ Good ❌ Bad
Add new subscribers without touching the sender Harder to trace who got what
Fully decoupled Eventual consistency
One event, many reactions

Swiggy: "order placed" → payment, restaurant, and analytics each get their own copy and react independently.
Azure: Service Bus Topic (with subscriptions), or Event Grid.

Queue vs Topic — lock this in:

Queue Topic
One message → one consumer One message → many subscribers
"Assign this task" "Announce this happened"

3. Competing Consumers

Multiple workers pull from the SAME queue to process faster. They compete for messages.

             ┌→ Worker 1
[ QUEUE ] ───┼→ Worker 2   (whoever's free grabs the next message)
             └→ Worker 3
Enter fullscreen mode Exit fullscreen mode
✅ Good ❌ Bad
Scale by adding workers Message order isn't guaranteed
Auto load-balances Workers must be stateless
Drains faster at peak

Swiggy: at dinner peak, 50,000 "charge order" messages pile up. Run 100 payment workers off one queue → it drains fast. Each message still goes to exactly one worker (no double-processing).
This is how you scale a queue consumer.


4. Dead-Letter Queue (DLQ)

A separate queue for messages that keep failing — so one bad message can't block everything.

The problem it solves is a poison message: one malformed message that fails, retries, fails, retries… forever, jamming the queue.

[ QUEUE ] → try process → fail → retry → fail (3x) → move to [ DLQ ]
                                                       ↑ inspect later; queue keeps flowing
Enter fullscreen mode Exit fullscreen mode
✅ Good ❌ Bad
One bad message can't block the line Needs monitoring
Failed messages saved for inspection Manual cleanup

Swiggy: a corrupt order message fails 3 times → it's shoved into the DLQ → the main queue keeps flowing for everyone else. An engineer inspects the DLQ later.
Azure: Service Bus has a DLQ built-in on every queue and subscription.

⚠️ The trap: teams set up a DLQ and never watch it. Messages die silently. Always alert on DLQ depth.


5. Idempotent Consumer

Processing the same message twice produces the SAME result as processing it once.

Why you need it: queues can deliver a message twice (a network hiccup, a retry). Without protection, "charge ₹450" runs twice → the customer is charged ₹900. 💥

Message "charge order #123" arrives twice:
  ❌ Not idempotent: charge ₹450 + charge ₹450 = ₹900 (disaster)
  ✅ Idempotent:     "already processed #123?" → skip the 2nd → ₹450 ✅
Enter fullscreen mode Exit fullscreen mode

How it works: track a unique message/operation ID; if you've seen it, skip it.

✅ Good ❌ Bad
Safe against duplicate delivery Must store processed IDs
Retries become harmless Adds a check on every message

Swiggy: every payment carries an idempotency key. A duplicate "charge" message is detected and skipped. Charged once.
This is non-negotiable in money flows — "at-least-once delivery" means duplicates will happen, so consumers must be idempotent.


How it all connects

Message Queue        → the basic buffer (1 → 1)
Publish-Subscribe    → the broadcast version (1 → many)
Competing Consumers  → scale the consumer side (many workers, 1 queue)
Dead-Letter Queue    → catch the failures
Idempotent Consumer  → survive duplicate deliveries
Enter fullscreen mode Exit fullscreen mode

The whole thing in one line

A queue assigns one task; a topic announces to all. Then you scale it (competing consumers), protect it (dead-letter queue), and make it safe to retry (idempotent consumer). Get those right and messaging becomes the most reliable way for services to talk.

Top comments (0)