Azure Service Bus for Event-Driven Systems
From https://dev.to/aldacosta/azure-service-bus-for-event-driven-systems-a-practical-deep-dive-5an3
What Is Azure Service Bus, and When Should You Reach for It?
Azure Service Bus is a cloud-native message broker supporting both message queuing and publish-subscribe patterns. It operates at the PaaS level — you don't manage infrastructure, brokers, or clusters. It provides:
Guaranteed message delivery with at-least-once semantics
FIFO ordering via sessions
Transactions across multiple operations
Dead-lettering and deferred message handling
Built-in duplicate detection
Message scheduling and delayed delivery
Service Bus vs. Event Grid vs. Event Hubs: Choosing the Right Tool
This is the question that comes up in every architecture review, so let's settle it with a decision framework.
Azure Service Bus is your choice when you need reliable command/message delivery between services. Think: "process this order," "send this notification," "update this record." It excels at transactional workloads where every message matters and must be processed exactly as intended.
Azure Event Grid is built for reactive event routing. It's ideal for lightweight, high-fanout notifications — "a blob was uploaded," "a resource was created." It's push-based, operates on a per-event pricing model, and is optimized for low-latency event distribution rather than queuing.
Azure Event Hubs is a high-throughput event streaming platform. If you're ingesting telemetry, logs, or clickstream data at millions of events per second and need to replay or process streams in order, Event Hubs (or its Kafka-compatible interface) is the right fit.
The decision heuristic: if losing a message is unacceptable and consumers need guaranteed processing → Service Bus. If you're distributing notifications reactively → Event Grid. If you're streaming high-volume data for analytics → Event Hubs.
In practice, production systems often combine all three. An order placed in Service Bus might trigger an Event Grid notification to update a dashboard, while telemetry from the process flows into Event Hubs for analytics.
Core Concepts in Depth
Queues vs. Topics vs. Subscriptions
Queues implement a point-to-point messaging pattern. A message sent to a queue is received by exactly one consumer. If multiple consumers are listening, they compete for messages — this is the competing consumers pattern, and it's how you scale processing horizontally.
Producer → [Queue] → Consumer A
→ Consumer B (competing; each message goes to one)
Topics and Subscriptions implement publish-subscribe. A message published to a topic is delivered to every subscription on that topic. Each subscription acts like a virtual queue with its own independent cursor. Subscriptions can have filters (SQL-like expressions or correlation filters) that determine which messages they receive.
Producer → [Topic] → Subscription A (filter: OrderType = 'Premium') → Consumer A
→ Subscription B (filter: Region = 'EU') → Consumer B
→ Subscription C (no filter — gets everything) → Consumer C
This distinction matters for your architecture: queues for work distribution, topics for event broadcasting with selective consumption.
Messages, Sessions, and Ordering
A Service Bus message consists of a binary body (up to 256 KB on Standard, 100 MB on Premium) and a set of broker-managed and user-defined properties. Properties are key-value pairs that ride alongside the payload without requiring deserialization — this is what makes subscription filters possible.
Sessions solve the ordering problem. Standard queues and subscriptions offer best-effort FIFO within a single partition, but no strict guarantees. When you need guaranteed ordering for a group of related messages, you assign them a common SessionId. All messages with the same session ID are delivered in order to a single consumer that holds an exclusive lock on that session.
A practical example: if you're processing events for a specific customer — account created, address updated, order placed — you set SessionId = customerId. This ensures those events are processed sequentially, even with multiple competing consumers handling different customers in parallel.
Dead-Letter Queues
Every queue and subscription has a companion dead-letter queue (DLQ) — a sidecar that captures messages that cannot be processed. Messages land in the DLQ when:
They exceed the maximum delivery count (too many processing failures)
Their TTL expires before being consumed
A subscription filter evaluation fails
The receiver explicitly dead-letters them (e.g., a poison message that fails validation)
The DLQ is not a trash can — it's an operations signal. Production systems need monitoring on DLQ depth and automated or semi-automated processes to inspect, remediate, and resubmit dead-lettered messages. Ignoring the DLQ is one of the most common operational mistakes in Service Bus deployments.
Message Delivery Guarantees
Service Bus provides at-least-once delivery by default. When a consumer receives a message in PeekLock mode, the message becomes invisible to other consumers but isn't removed from the queue. The consumer must explicitly complete the message after successful processing. If the lock expires or the consumer crashes, the message becomes visible again and is redelivered.
The alternative is ReceiveAndDelete mode — the message is removed from the queue immediately upon delivery. This gives you at-most-once semantics with lower latency, but no safety net. Use it only when losing occasional messages is acceptable (e.g., non-critical telemetry).
Duplicate detection is a broker-side feature that prevents the same message from being enqueued twice within a configurable time window. It works by tracking the MessageId property. This is invaluable when producers might retry sends after ambiguous failures (network timeouts, for instance), but it only deduplicates at the ingestion side — it doesn't prevent a consumer from processing the same message twice after redelivery.
Scheduling and Delayed Delivery
Service Bus supports scheduled enqueue time — you can send a message now but have it become visible to consumers at a future point in time. This is implemented broker-side, which means your producer doesn't need to maintain timers or polling loops.
Use cases include: delaying a retry after a transient failure, scheduling a reminder notification, implementing a timeout pattern ("if the order isn't confirmed within 30 minutes, cancel it"), or staging messages for batch processing at a specific time window.
// Schedule a message for 30 minutes from now
var sequenceNumber = await sender.ScheduleMessageAsync(
message,
DateTimeOffset.UtcNow.AddMinutes(30));
// Cancel it if needed before it fires
await sender.CancelScheduledMessageAsync(sequenceNumber);
Decoupling and Scalability in Microservices
The real value of Service Bus in a microservices architecture goes beyond "services don't call each other directly." Here's what decoupling actually gives you in practice:
Temporal decoupling: the producer and consumer don't need to be running at the same time. Your API can accept and enqueue an order even if the fulfillment service is down for deployment. The queue absorbs the gap.
Load leveling: during a flash sale, your web tier might enqueue thousands of orders per second. Your processing tier can consume them at a sustainable rate without being overwhelmed. The queue acts as a shock absorber.
Independent scaling: queue consumers can be scaled out horizontally. With competing consumers, you simply add more instances. Each instance pulls messages independently. Azure Container Apps, Azure Functions, or KEDA-scaled Kubernetes pods can auto-scale consumer count based on queue depth.
Independent deployment: because services communicate through messages (contracts) rather than direct API calls, you can deploy, version, and scale them independently. A schema change on the producer side doesn't require a synchronized deployment on the consumer side — as long as the message contract is honored.
Real-World Scenarios
Scenario 1: Order Processing Pipeline
An e-commerce platform decomposes order processing into discrete stages: validation, payment, inventory reservation, and fulfillment. Each stage is a separate service. The order flows through a series of queues:
API Gateway → [orders-validation] → Validation Service
↓
[orders-payment] → Payment Service
↓
[orders-fulfillment] → Fulfillment Service
Each service reads from its input queue, performs its work, and publishes to the next queue (or to a topic if multiple downstream services need to react). Failures at any stage result in retries via the lock mechanism or dead-lettering for manual review. The entire pipeline is resilient to individual service outages.
Scenario 2: Cross-Service Integration Events
A SaaS platform publishes domain events (e.g., UserRegistered, SubscriptionUpgraded) to a Service Bus topic. Multiple downstream services subscribe selectively:
The email service subscribes to
UserRegisteredto send welcome emailsThe billing service subscribes to
SubscriptionUpgradedto adjust invoicingThe analytics service subscribes to all events for audit logging
Each subscription has its own filter and processes at its own pace. Adding a new consumer means adding a new subscription — no changes to the producer.
Scenario 3: Background Job Offloading
A web API needs to generate PDF reports, a CPU-intensive operation. Instead of blocking the HTTP request, it enqueues a GenerateReport message and returns 202 Accepted with a job ID. A background worker pool processes the queue, generates the PDF, uploads it to blob storage, and publishes a completion event. The client polls or subscribes for the result.
Architecture Patterns
Publish-Subscribe with Filtered Subscriptions
OrderService → [order-events topic]
→ Subscription: "billing" (filter: Subject = 'OrderPlaced') → BillingService
→ Subscription: "shipping" (filter: Amount > 100) → ShippingService
→ Subscription: "analytics" (no filter) → AnalyticsService
Each downstream service gets exactly the events it cares about. Adding a new consumer is a subscription configuration change — no code changes to the publisher.
Competing Consumers for Horizontal Scaling
[orders-queue] → Consumer Instance 1 (auto-scaled by KEDA / Azure Functions)
→ Consumer Instance 2
→ Consumer Instance 3
→ ...
All instances read from the same queue. The broker ensures each message is delivered to exactly one instance. Scale the instance count based on queue depth using KEDA (Kubernetes), Azure Functions auto-scale, or Azure Container Apps scaling rules.
Saga/Choreography with Service Bus
For distributed transactions across services (e.g., order → payment → inventory), each service publishes domain events after completing its step. Compensating actions handle failures:
OrderService: publishes OrderPlaced
→ PaymentService: processes, publishes PaymentConfirmed OR PaymentFailed
→ InventoryService: reserves stock, publishes StockReserved OR StockUnavailable
→ If failure at any stage → compensating events roll back prior steps
Sessions ensure ordering per saga instance. Dead-letter queues capture stuck sagas for manual intervention.
Request-Reply Over Service Bus
When you need asynchronous request-reply (the caller expects a response, but not synchronously), use the ReplyTo and ReplyToSessionId properties:
// Sender sets up a temporary reply queue
var request = new ServiceBusMessage(payload)
{
ReplyTo = "reply-queue",
ReplyToSessionId = Guid.NewGuid().ToString(),
MessageId = correlationId
};
await sender.SendMessageAsync(request);
// Receiver processes and replies
var reply = new ServiceBusMessage(responsePayload)
{
SessionId = args.Message.ReplyToSessionId,
CorrelationId = args.Message.MessageId
};
await replySender.SendMessageAsync(reply);
Summary
Azure Service Bus is the backbone of reliable, asynchronous communication in Azure-based distributed systems. Its strength lies in the combination of guaranteed delivery, flexible routing (queues and topics), session-based ordering, and enterprise-grade features like dead-lettering, duplicate detection, and scheduling — all without infrastructure management overhead.
The key decision points are: use queues for point-to-point work distribution, topics for event broadcasting with selective consumption, sessions when ordering matters, and Premium tier when you need predictable performance and network isolation.