Message Queue Fundamentals
📖 Concept
A message queue is a middleware that enables asynchronous communication between services. Instead of Service A calling Service B directly (synchronous), Service A puts a message on the queue, and Service B processes it when ready.
Why Message Queues?
| Problem | How Queues Solve It |
|---|---|
| Tight coupling | Services communicate via messages, not direct calls |
| Overwhelming downstream | Queue acts as buffer, consumer processes at its own pace |
| Retry complexity | Queue retries failed messages automatically |
| Availability dependency | Producer works even if consumer is down |
| Traffic spikes | Queue absorbs bursts, consumer processes steadily |
Core Concepts
- Producer: Creates and sends messages to the queue
- Consumer: Reads and processes messages from the queue
- Queue/Topic: The channel that holds messages
- Broker: The server managing queues (RabbitMQ, Kafka, SQS)
- Dead Letter Queue (DLQ): Where failed messages go after max retries
Delivery Guarantees
| Guarantee | Description | Example |
|---|---|---|
| At-most-once | Message may be lost, never duplicated | Logging, metrics |
| At-least-once | Message delivered 1+ times, may have duplicates | Order processing (with idempotency) |
| Exactly-once | Message delivered exactly once | Payment processing (hardest to achieve) |
Queue vs Topic (Pub/Sub)
| Pattern | Queue (Point-to-Point) | Topic (Pub/Sub) |
|---|---|---|
| Consumers | One consumer gets each message | All subscribers get every message |
| Use case | Task distribution, work queues | Event broadcasting, notifications |
| Example | Email sending queue | "Order created" event to inventory, shipping, and analytics |
Pro tip: In interviews, use message queues whenever you see "process this later," "notify multiple services," or "handle traffic spikes."
💻 Code Example
1// ============================================2// Message Queue Patterns3// ============================================45// ---------- Basic Queue Pattern ----------6class MessageQueue {7 constructor() {8 this.queues = new Map();9 this.deadLetterQueue = [];10 }1112 publish(queueName, message) {13 if (!this.queues.has(queueName)) this.queues.set(queueName, []);14 this.queues.get(queueName).push({15 id: Math.random().toString(36).slice(2),16 data: message,17 timestamp: Date.now(),18 retries: 0,19 maxRetries: 3,20 });21 }2223 async consume(queueName, handler) {24 const queue = this.queues.get(queueName) || [];25 while (queue.length > 0) {26 const message = queue.shift();27 try {28 await handler(message.data);29 console.log(`✅ Processed: \${message.id}`);30 } catch (error) {31 message.retries++;32 if (message.retries < message.maxRetries) {33 queue.push(message); // Retry34 console.log(`⏳ Retry \${message.retries}/\${message.maxRetries}: \${message.id}`);35 } else {36 this.deadLetterQueue.push(message); // Move to DLQ37 console.log(`❌ DLQ: \${message.id} after \${message.maxRetries} retries`);38 }39 }40 }41 }42}4344// ---------- Async Order Processing ----------4546// ❌ BAD: Synchronous — user waits for everything47async function processOrderSync(order) {48 await validateOrder(order); // 50ms49 await chargePayment(order); // 200ms50 await updateInventory(order); // 100ms51 await sendConfirmationEmail(order); // 300ms52 await notifyWarehouse(order); // 150ms53 await updateAnalytics(order); // 100ms54 return { status: 'completed' }; // 900ms total!55}5657// ✅ GOOD: Async — user gets fast response58async function processOrderAsync(order, queue) {59 await validateOrder(order); // 50ms60 const payment = await chargePayment(order); // 200ms6162 if (payment.success) {63 // Queue remaining tasks64 await queue.publish('order.confirmed', {65 orderId: order.id, items: order.items, userId: order.userId,66 });67 return { status: 'confirmed' }; // 250ms total!68 }69 return { status: 'payment_failed' };70}7172// Background workers process events independently:73// Worker 1: order.confirmed → updateInventory74// Worker 2: order.confirmed → sendConfirmationEmail75// Worker 3: order.confirmed → notifyWarehouse76// Worker 4: order.confirmed → updateAnalytics7778// ---------- Fan-out Pattern (One event → Multiple consumers) ----------79class EventBus {80 constructor() { this.subscribers = new Map(); }8182 subscribe(eventType, handler) {83 if (!this.subscribers.has(eventType)) this.subscribers.set(eventType, []);84 this.subscribers.get(eventType).push(handler);85 }8687 async publish(eventType, data) {88 const handlers = this.subscribers.get(eventType) || [];89 await Promise.all(handlers.map(h => h(data)));90 }91}9293// Usage94const bus = new EventBus();95bus.subscribe('user.registered', async (data) => { console.log('Send welcome email:', data.email); });96bus.subscribe('user.registered', async (data) => { console.log('Create default settings:', data.userId); });97bus.subscribe('user.registered', async (data) => { console.log('Track analytics:', data.userId); });98bus.publish('user.registered', { userId: '123', email: 'user@test.com' });99100async function validateOrder(o) {}101async function chargePayment(o) { return { success: true }; }102async function updateInventory(o) {}103async function sendConfirmationEmail(o) {}104async function notifyWarehouse(o) {}105async function updateAnalytics(o) {}
🏋️ Practice Exercise
Async Refactor: Refactor a synchronous user registration flow (validate → create user → send email → create settings → log analytics) into async using message queues.
Dead Letter Queue: Design a DLQ handler for a payment processing queue. What happens to failed payments? How do you retry? Alert? Manually resolve?
Delivery Guarantees: For each scenario, choose at-most-once, at-least-once, or exactly-once: (a) logging, (b) payment, (c) email notification, (d) inventory update.
Queue vs Direct Call: When should you use a message queue vs direct HTTP call between services? Give 3 examples of each.
⚠️ Common Mistakes
Using queues for everything — synchronous calls are simpler when both services must be available and response time matters. Don't add a queue between your API and its database.
Not handling message failures — without retry logic and dead letter queues, failed messages are silently lost. Always configure retries with exponential backoff and DLQs.
Ignoring message ordering — most queues don't guarantee order across partitions. If order matters, use a single partition or sequence numbers with consumer-side reordering.
Not making consumers idempotent — with at-least-once delivery, consumers may process the same message twice. Use idempotency keys to prevent duplicate side effects.
💼 Interview Questions
🎤 Mock Interview
Practice a live interview for Message Queue Fundamentals