You are viewing a free preview of this lesson.
Subscribe to unlock all 10 lessons in this course and every other course on LearningBro.
Understanding the Azure messaging services is only half the challenge. Applying proven patterns and best practices is what separates a reliable production system from a fragile one. This lesson covers essential messaging patterns, error-handling strategies, security practices, and operational guidelines for building robust event-driven architectures on Azure.
Multiple consumers read from the same queue, distributing the workload:
Producer --> [ Queue ] --> Consumer 1
--> Consumer 2
--> Consumer 3
When to use: Any queue-based workload where you need horizontal scaling. Each message is processed by exactly one consumer.
Implementation: Deploy multiple instances of your consumer application, all reading from the same Service Bus queue. Service Bus handles distribution automatically.
Best practice: Scale consumers based on queue depth. Use Azure Monitor metrics (ActiveMessageCount) to trigger auto-scaling.
One event is delivered to multiple independent subscribers:
Publisher --> [ Topic ] --> Subscription A --> Service A
--> Subscription B --> Service B
--> Subscription C --> Service C
When to use: When multiple services need to react to the same event without knowing about each other.
Implementation: Use Service Bus topics or Event Grid. Add filters to subscriptions so each service only receives relevant events.
A producer sends a message and expects a response on a separate queue:
Client --> [ Request Queue ] --> Server
Client <-- [ Reply Queue ] <-- Server
Implementation: Set the replyTo property to the reply queue name and correlationId to match responses to requests:
await sender.sendMessages({
body: { question: 'What is the status of order 123?' },
replyTo: 'reply-queue',
correlationId: 'req-001',
sessionId: 'req-001',
});
Best practice: Use sessions on the reply queue so the client receives only its own responses.
For messages that exceed size limits, store the payload externally and send a reference:
Producer --> [ Blob Storage ] --> (stores large payload, gets URL)
--> [ Queue ] --> { "blobUrl": "https://..." }
Consumer --> reads blob URL from message --> downloads payload from Blob Storage
When to use: When message payloads exceed the broker's size limit (256 KB for Service Bus Standard, 1 MB for Event Grid/Event Hubs).
Coordinate a multi-step business process across multiple services using compensating transactions:
Order Created --> Charge Payment --> Reserve Stock --> Confirm Order
| |
v (on failure) v (on failure)
Refund Payment Release Stock
Implementation: Use Service Bus queues with sessions to process each step in order. If a step fails, send compensating commands to undo previous steps.
Best practice: Make each step idempotent so retries do not cause side effects.
Build a dedicated service to handle dead-lettered messages:
[ Queue ] --> Consumer --> (fails maxDeliveryCount times) --> [ DLQ ]
|
v
DLQ Processor --> Alert + Investigate + Replay
Implementation: Create a consumer for the dead-letter sub-queue. Log the failure details, alert the team, and provide a mechanism to replay the message after the issue is fixed.
Idempotency means that processing the same message multiple times produces the same result as processing it once. This is critical because at-least-once delivery means your consumer will receive duplicate messages in edge cases.
Strategies for idempotency:
| Strategy | Example |
|---|---|
| Idempotency key | Store processed message IDs in a database; skip if already seen |
| Upsert operations | Use database upserts instead of inserts |
| Conditional writes | Use ETags or version numbers for optimistic concurrency |
| Natural idempotency | Some operations are naturally idempotent (e.g., setting a value) |
// Example: idempotency check with a database
const messageId = msg.messageId;
const existing = await db.processedMessages.findUnique({ where: { id: messageId } });
if (existing) {
await receiver.completeMessage(msg); // Already processed, skip
return;
}
await processOrder(msg.body);
await db.processedMessages.create({ data: { id: messageId } });
await receiver.completeMessage(msg);
Subscribe to continue reading
Get full access to this lesson and all 10 lessons in this course.