Notes
The problem
The direct version: the order service calls email, inventory and analytics one after another. The user waits for all of them. If analytics is down, do orders fail? And on a sale day, a spike hits every downstream service at the same moment. The services are tightly coupled.
- order request calls 3 services
- latency = sum of the slow ones
- availability = product of all
- analytics down → orders fail?
- Black Friday spike → everything
- downstream melts at once
The queue
With a queue, the order service writes one message, 'order placed', to a durable queue and returns immediately. Consumers pick it up when they can. The producer doesn't know or wait for them. A spike just makes the queue longer. And if a consumer is down, its messages wait for it instead of being lost.
- decoupling: producer does not
- know or wait for consumers
- buffering: a spike fills the
- queue, workers drain it at
- their own pace
- reliability: messages wait
- while a consumer is down
Queue vs pub-sub
Two shapes. A work queue hands each message to exactly one worker; add workers to go faster. That's SQS or a RabbitMQ queue, perfect for 'resize this image'. Pub-sub is a topic: every subscribing service gets its own copy. Email, inventory and analytics all need to see 'order placed', so that's pub-sub.
- work queue (point-to-point):
- each message goes to ONE
- worker; add workers to
- scale (SQS, RabbitMQ)
- pub-sub (topic):
- each message goes to EVERY
- subscriber group
- (Kafka, SNS, Pub/Sub)
Kafka
Kafka works differently from a classic queue. A topic is an append-only log. Messages get an offset and are kept for days, not deleted when read. Each consumer group just remembers its own offset: email has processed up to four, analytics up to two. A new service can even replay from the start.
topic "orders", partition 0:
offset: 0 1 2 3 4
o17 o18 o19 o20 o21
messages are appended and kept
(days), not deleted on read
each consumer group tracks its
own offset: email at 4,
analytics at 2 Partitions
To scale, a topic is split into partitions, and a message's key, say the order id, picks its partition. Within a consumer group each partition is read by one worker, so three partitions allow three parallel workers. Order is guaranteed within a partition, not across the whole topic. So put everything that must stay in order under the same key.
topic "orders": 3 partitions key = order_id → partition group "email" with 3 workers: worker 1 ← partition 0 worker 2 ← partition 1 worker 3 ← partition 2 order is kept WITHIN a partition max parallelism = partitions
Guarantees
Delivery guarantees. At-most-once: acknowledge before processing, and a crash loses the message. At-least-once: acknowledge after processing, and a crash means it's delivered again. That's the normal default, so consumers will see duplicates. 'Exactly-once' in practice means at-least-once plus a consumer that safely ignores repeats, which is episode twenty-nine.
- at-most-once: ack before
- processing → crash loses it
- at-least-once: ack after
- processing → crash means
- it is delivered again
- (the usual default)
- exactly-once: really
- "effectively once" =
- at-least-once + idempotent
- consumer (episode 29)
Dead letters
Some messages always fail: a bad email address, a bug for one weird order. Retried forever, that poison message blocks everything behind it. So retry a few times with growing delays, then move it to a dead-letter queue. Alert when the dead-letter queue grows, fix the bug, and replay the messages.
- a message that always fails
- ("poison") would block the
- queue forever
- retry with backoff, N times
- then move it to a
- dead-letter queue (DLQ)
- alert on DLQ size,
- fix, replay
Lag
The one metric to watch is consumer lag: how far a group's position is behind the newest message. Growing lag means consumers can't keep up, so add workers, up to the number of partitions. A queue absorbs a spike for a while; it doesn't make a permanently overloaded system fast.
- consumer lag = newest offset
- minus the group’s offset
- growing lag → consumers too
- slow: add workers (up to the
- number of partitions)
- backpressure: a queue hides
- overload for a while,
- not forever
Decision
When to use one: when the user doesn't need the result right now, like emails, thumbnails and analytics; when traffic is spiky; and when several services react to the same event. Keep it synchronous when the user is waiting for the answer, like authorising a payment or logging in.
- use a queue when:
- • the user does not need the
- result right now (email,
- thumbnails, analytics)
- • bursts are spiky
- • several services react to
- one event
- keep it sync when the user
- needs the answer (payment
- authorisation, login)
Say it like this
In the interview: publish an event to a topic, partitioned by the entity id so related events stay in order. Assume at-least-once delivery and make consumers idempotent. Retry with backoff and send failures to a dead-letter queue. And monitor consumer lag. That's a complete answer in one breath.
- "How do you handle the slow
- side effects?"
- publish an event to a topic,
- partition by entity id,
- at-least-once delivery,
- idempotent consumers,
- retries + DLQ,
- monitor consumer lag