Event Driven Architectures in Azure
Event-driven architecture is not a single pattern — it is a family of patterns that share a common idea: services react to things that happened rather than being commanded to do things. This shift from imperative (“do this now”) to reactive (“respond when this occurs”) produces systems that are more loosely coupled, easier to scale, and more resilient to individual service failures. Azure provides three distinct eventing services, each suited to different communication patterns, and understanding when to use each is the foundation of building good event-driven systems on Azure.
The three Azure eventing services
| Service | Pattern | Delivery | Throughput | Best for |
|---|---|---|---|---|
| Azure Event Grid | Pub/sub, discrete events | Push (to webhooks, Functions, Logic Apps) | Millions of events/sec | React to resource changes, trigger serverless functions, routing events |
| Azure Event Hubs | Streaming, time-series events | Pull (consumers poll partitions) | Millions of events/sec, sustained | Telemetry, logs, clickstream, IoT data pipelines |
| Azure Service Bus | Message queuing, pub/sub | Push or pull | Millions of messages/day | Reliable business transactions, work queues, ordered processing |
A real-world architecture often uses all three in different parts of the same system. An e-commerce platform might use Event Hubs to ingest user clickstream data for analytics, Event Grid to trigger an Azure Function when a new order document is created in Blob Storage, and Service Bus to reliably pass the “process payment” task between the order service and the payment service.
Azure Event Grid: reactive event routing
Event Grid is a serverless event routing service. It receives events from event sources (Azure services like Storage, Cosmos DB, Resource Manager, or your own applications) and routes them to event handlers (Azure Functions, Logic Apps, Service Bus queues, webhooks, or Event Hubs).
Event Grid delivers events via HTTP POST to subscribed handlers. It retries delivery using an exponential backoff schedule for up to 24 hours. Events that cannot be delivered can be dead-lettered to a Storage account.
# Subscribe an Azure Function to blob storage events
# (triggers the function whenever a new file is created in the container)
# First, get the function's webhook endpoint
FUNC_URL=$(az functionapp function show \
--resource-group rg-eventdriven \
--name func-processorders \
--function-name ProcessNewOrder \
--query "invokeUrlTemplate" \
--output tsv)
# Create an Event Grid subscription for blob creation events
az eventgrid event-subscription create \
--name sub-blob-to-function \
--source-resource-id "/subscriptions/{sub}/resourceGroups/rg-eventdriven/providers/Microsoft.Storage/storageAccounts/mystorageacct" \
--endpoint-type webhook \
--endpoint $FUNC_URL \
--included-event-types Microsoft.Storage.BlobCreated \
--subject-begins-with "/blobServices/default/containers/orders/"With this subscription in place, every time a new file lands in the orders/ container of the storage account, Event Grid delivers a notification to the Azure Function. The function processes the file without needing to poll the container.
Azure Service Bus: reliable business messaging
Service Bus is a fully managed message broker designed for enterprise messaging patterns. Unlike Event Hubs (streaming, pull-based, no per-message guarantees) and Event Grid (push delivery, best-effort retry), Service Bus provides:
- At-least-once or exactly-once delivery guarantees
- Dead-letter queue — messages that cannot be processed are moved to a dead-letter sub-queue for investigation
- Message sessions — group messages by session ID to guarantee ordered processing per entity (e.g., all messages for a given order processed in order)
- Message deferral — a receiver can defer a message to process it later while continuing to process other messages
- Duplicate detection — prevents the same message from being processed twice based on a message ID
# Send a message to a Service Bus queue
from azure.servicebus import ServiceBusClient, ServiceBusMessage
import json
connection_string = "Endpoint=sb://sb-ns-mycompany.servicebus.windows.net/;..."
queue_name = "order-processing"
with ServiceBusClient.from_connection_string(connection_string) as client:
with client.get_queue_sender(queue_name=queue_name) as sender:
order_message = ServiceBusMessage(
body=json.dumps({
"order_id": 1001,
"customer_id": 42,
"total": 149.99,
"items": [{"product_id": "SKU-123", "quantity": 2}]
}),
message_id="order-1001", # for duplicate detection
session_id="customer-42", # process all messages for customer 42 in order
subject="new-order"
)
sender.send_messages(order_message)
print("Order message sent to Service Bus queue")The Saga pattern for distributed transactions
In a distributed event-driven system, a business operation like “place an order” might span multiple services — inventory, payment, shipping, notifications. A traditional database transaction cannot span service boundaries. The Saga pattern replaces a distributed transaction with a sequence of local transactions, each publishing an event when it completes. If any step fails, compensating events trigger rollbacks in preceding steps.
Order Saga:
1. OrderService receives "PlaceOrder" command
→ Publishes "OrderCreated" event
2. InventoryService consumes "OrderCreated"
→ Reserves items
→ Publishes "InventoryReserved" event
OR
→ Cannot reserve (out of stock)
→ Publishes "InventoryReservationFailed" event
3a. PaymentService consumes "InventoryReserved"
→ Charges customer
→ Publishes "PaymentProcessed" event
OR
→ Charge fails
→ Publishes "PaymentFailed" event
3b. If "InventoryReservationFailed" or "PaymentFailed":
→ OrderService consumes failure event
→ Publishes "OrderCancelled" event
→ InventoryService releases reserved items on "OrderCancelled"
4. ShippingService consumes "PaymentProcessed"
→ Creates shipment
→ Publishes "ShipmentCreated" event
5. NotificationService consumes "PaymentProcessed" and "ShipmentCreated"
→ Sends confirmation and tracking emailsIn Azure, each step uses a Service Bus topic subscription to receive its triggering event and publishes the outcome event back to Service Bus or Event Grid. Each service manages its own local database transaction, ensuring that its own state is always consistent even if the overall saga is in a partially completed state.
Event sourcing: events as the source of truth
Traditional systems store the current state of data: “customer 42 has a balance of $150.” Event sourcing stores the events that led to that state: “customer 42 deposited $200, then withdrew $50 — balance is $150.” The event log is the source of truth, and current state is derived by replaying the events.
Event Hubs is well-suited as the event store when combined with its Capture feature. All events are stored durably in ADLS Gen2. A new service can replay all historical events to build its own projection of the current state. This is called a projection or read model.
# Simplified event sourcing: rebuild account balance from events
from azure.eventhub import EventHubConsumerClient, EventPosition
import json
def rebuild_account_balances():
"""Replay all events from the beginning to reconstruct current balances"""
balances = {}
with EventHubConsumerClient.from_connection_string(
conn_str="Endpoint=sb://...",
consumer_group="balance-rebuild-cg",
eventhub_name="account-events"
) as client:
def process_event(partition_context, event):
payload = json.loads(event.body_as_str())
account_id = payload["account_id"]
event_type = payload["event_type"]
amount = payload.get("amount", 0)
if account_id not in balances:
balances[account_id] = 0
if event_type == "deposit":
balances[account_id] += amount
elif event_type == "withdrawal":
balances[account_id] -= amount
elif event_type == "fee":
balances[account_id] -= amount
# Read from the earliest available event
client.receive(
on_event=process_event,
starting_position="@latest" if False else "-1", # -1 = earliest
max_wait_time=10 # Stop after 10 seconds of no new events
)
return balances
current_balances = rebuild_account_balances()
print(f"Rebuilt balances for {len(current_balances)} accounts")Choosing the right pattern for your use case
| Use case | Recommended service/pattern | Why |
|---|---|---|
| Process file when it lands in storage | Event Grid + Azure Functions | Push delivery to function; no polling; pay per execution |
| Ingest IoT sensor telemetry (10,000 devices) | Event Hubs + Stream Analytics | High-throughput pull; partitioned for parallel consumers |
| Reliable payment processing between microservices | Service Bus queue + sessions | Exactly-once, ordered, dead-letter for failures |
| Distributed saga across 5 microservices | Service Bus topics + subscriptions | Pub/sub decouples services; dead-letter handles failed steps |
| Real-time clickstream analytics | Event Hubs + Spark Structured Streaming | High-throughput streaming; Spark handles complex aggregations |
| Audit log that every service can query | Event Hubs Capture + ADLS Gen2 | Durable, immutable archive; queryable via Synapse serverless SQL |
Common mistakes
- Using Event Grid for high-throughput streaming. Event Grid is designed for discrete events at moderate volume. Sending millions of events per minute to Event Grid is expensive and inefficient. Use Event Hubs for high-throughput continuous streams.
- Not designing for eventual consistency. In event-driven systems, services are eventually consistent. If your UI shows data from Service A and data from Service B that are both updated by the same event, they may temporarily show different values. Design your UI to handle “pending” states gracefully rather than assuming immediate consistency.
- Building a “chatty” event-driven system. Every request resulting in dozens of small events between services adds latency and complexity. Group logically related state changes into a single “domain event” (e.g., “order fully placed” with all line items) rather than emitting separate events for each field update.
- Not handling duplicate events. All three Azure messaging services can deliver the same message more than once in failure scenarios. Every event consumer must be idempotent — processing the same event twice should produce the same result as processing it once. Store event_ids you have already processed and skip duplicates.
Summary
- Event-driven architectures decouple services by having them communicate through events rather than direct API calls.
- Azure Event Grid routes discrete events via push to serverless functions and webhooks — best for reactive triggers.
- Azure Event Hubs handles high-throughput streaming via pull — best for telemetry, logs, and analytics pipelines.
- Azure Service Bus provides reliable enterprise messaging with dead-lettering, sessions, and ordering — best for business transactions.
- The Saga pattern enables distributed transactions across microservices using compensating events instead of two-phase commit.
- All event consumers must be idempotent — duplicate event delivery is normal and must be handled gracefully.
Frequently asked questions
What is an event-driven architecture?
An event-driven architecture is a design pattern where services communicate by publishing and consuming events rather than calling each other directly via APIs. A service emits an event when something notable happens (e.g., "order placed") and other services subscribe to react to that event (e.g., update inventory, charge payment, send confirmation email). Services are decoupled — they do not need to know about each other.
When should I use Event Grid vs Event Hubs vs Service Bus?
Use Event Grid for reactive event routing to serverless functions and webhooks — it pushes events to subscribers and is best for discrete events (file created, resource changed, 1–10 KB payloads). Use Event Hubs for high-throughput telemetry streaming where consumers pull batches at their own pace. Use Service Bus for reliable application-to-application messaging with ordering, dead-lettering, and exactly-once delivery guarantees.
What is eventual consistency and why does it matter in event-driven systems?
Eventual consistency means that after an event is processed by all consumers, the system as a whole will be consistent — but there is a window of time where different services hold different views of the data. For example, after "order placed" is published, the inventory service may decrement stock 100ms before the email service sends the confirmation. Designing for eventual consistency means accepting this transient inconsistency and ensuring downstream services converge to the correct state.