On This Page
1Overview2Architecture Overview
3Azure Service Topology4Implementation Guide
5Decision Criteria6Cost Model
7Anti-Patterns to Avoid8References

Overview

In an event-driven architecture, services communicate by publishing events to a bus or queue rather than calling each other directly. The producing service does not know who consumes its events, and consumers do not know who produces them. This inversion of control eliminates the tight coupling that makes monolithic and synchronous microservice systems fragile at scale.

Azure implements this pattern using three purpose-built services: Event Grid for reactive pub/sub routing of discrete domain events, Service Bus for enterprise-grade ordered command delivery where guaranteed processing matters, and Event Hubs for high-throughput telemetry and log streaming at Kafka-protocol scale. Choosing the right service for each flow — and combining them — is the primary architectural decision.

Architecture Overview

The conceptual pattern separates the architecture into three zones: producers that emit events, the event routing layer that filters and routes based on content, and consumers that process events independently and idempotently.

%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','fontFamily':'IBM Plex Sans, system-ui, sans-serif','primaryColor':'#DBEAFE','primaryTextColor':'#1e3a5f','primaryBorderColor':'#2563EB','lineColor':'#374151','clusterBkg':'#F9FAFB','clusterBorder':'#D1D5DB','edgeLabelBackground':'#FFFFFF'},'flowchart':{'curve':'orthogonal','padding':30,'nodeSpacing':65,'rankSpacing':75,'useMaxWidth':true}}}%% flowchart TD START([Business Event Occurs]) subgraph PRODUCERS["Event Producers"] P1[Order Service] P2[Payment Service] P3[Telemetry Agent] end subgraph ROUTING["Event Routing Layer"] BUS[Messaging Tier\nContent-based routing\nSchema validation] RULE1{Domain Events} RULE2{Command Messages} RULE3{Streaming Data} end subgraph CONSUMERS["Event Consumers — Independent and Idempotent"] C1[Notification Service\nCustomer alerts] C2[Analytics Service\nBusiness intelligence] C3[Audit Service\nCompliance logging] C4[Fulfillment Service\nWarehouse trigger] end DLQ[Dead Letter Store\nFailed event storage\nReplay capability] END([System State Updated — No Direct Coupling]) START --> P1 & P2 & P3 P1 & P2 & P3 -->|Publish event| BUS BUS --> RULE1 & RULE2 & RULE3 RULE1 -->|Route| C1 & C2 RULE2 -->|Route| C3 & C4 RULE3 -->|Route| C2 & C3 C1 & C2 & C3 & C4 -->|On failure| DLQ C1 & C2 & C3 & C4 --> END style START fill:#4f8ef7,color:#fff style END fill:#10b981,color:#fff style DLQ fill:#fef3c7 style BUS fill:#e0f2fe

Azure Service Topology

On Azure, the event-driven pattern maps to three distinct managed services matched to three distinct flow types. Event Grid handles pub/sub routing of discrete domain events using CloudEvents schema with serverless per-operation pricing. Service Bus handles commands where ordered delivery, duplicate detection, and guaranteed processing are required — Premium tier is mandatory in production for private endpoints, zone redundancy, and session support. Event Hubs handles high-throughput telemetry and log aggregation using the Kafka protocol, with up to 90-day retention and full replay; partition count is fixed at creation and should be sized at 2× peak consumer parallelism.

%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','fontFamily':'IBM Plex Sans, system-ui, sans-serif','primaryColor':'#DBEAFE','primaryTextColor':'#1e3a5f','primaryBorderColor':'#2563EB','lineColor':'#374151','clusterBkg':'#F9FAFB','clusterBorder':'#D1D5DB','edgeLabelBackground':'#FFFFFF'},'flowchart':{'curve':'orthogonal','padding':30,'nodeSpacing':65,'rankSpacing':75,'useMaxWidth':true}}}%% flowchart TD SOURCE[Application or Azure Service\nPublishes CloudEvents payload] subgraph EG["Azure Event Grid"] TOPIC[Custom Topic\nCloudEvents 1.0 schema\nPublic network disabled] FILTER[Event Subscription\nincludedEventTypes filter\nRetry: 30 attempts / 24h] end subgraph SB["Azure Service Bus Premium"] SBT[Topic\nrequiresDuplicateDetection\nsupportOrdering enabled] SBSUB[Subscription\nmaxDeliveryCount: 10\nlockDuration: PT5M] SBDLQ[Dead-Letter Subqueue\ndeadLetteringOn\nMessageExpiration] end subgraph EH["Azure Event Hubs"] EHN[Event Hubs Namespace\nKafka protocol enabled\nRetention: up to 90 days] EHP[Partitions\nFixed at creation\nSized 2× peak parallelism] end subgraph CONSUMERS["Consumers — Independent and Idempotent"] CS1[Azure Function\nDomain event handler\nIdempotency via Cosmos DB] CS2[Azure Function\nCommand processor\nOrdered per session] CS3[Azure Stream Analytics\nTelemetry aggregation\nReal-time windowing] end BLOB[Azure Blob Storage\nDead-letter archive\nEvent Grid DLQ destination] SOURCE --> TOPIC TOPIC --> FILTER FILTER -->|Route to queue| SBT SBT --> SBSUB SBSUB -->|Deliver| CS2 SBSUB -->|MaxDeliveryCount exceeded| SBDLQ SBDLQ -->|Archive| BLOB FILTER -->|Dead-letter| BLOB SOURCE --> EHN EHN --> EHP EHP --> CS3 CS1 --> BLOB style SOURCE fill:#4f8ef7,color:#fff style SBDLQ fill:#fef3c7 style BLOB fill:#fef3c7 style FILTER fill:#e0f2fe style EHN fill:#d1fae5

Implementation Guide

Bicep — Service Bus Premium Namespace, Topic, and Subscription

This module provisions a zone-redundant Service Bus Premium namespace using Managed Identity only (local auth disabled), a topic configured for duplicate detection and ordered delivery, and a subscription with dead-lettering and a 5-minute lock duration.

param location string = resourceGroup().location
param namespaceName string
param topicName string = 'orders'
param subscriptionName string = 'order-processor'

resource sbNamespace 'Microsoft.ServiceBus/namespaces@2022-10-01-preview' = {
  name: namespaceName
  location: location
  sku: {
    name: 'Premium'
    tier: 'Premium'
    capacity: 1
  }
  properties: {
    minimumTlsVersion: '1.2'
    zoneRedundant: true
    disableLocalAuth: true   // Managed Identity only — no SAS keys
  }
}

resource sbTopic 'Microsoft.ServiceBus/namespaces/topics@2022-10-01-preview' = {
  parent: sbNamespace
  name: topicName
  properties: {
    requiresDuplicateDetection: true
    duplicateDetectionHistoryTimeWindow: 'PT10M'
    supportOrdering: true
    defaultMessageTimeToLive: 'P14D'
    enablePartitioning: false   // Partitioning incompatible with sessions
  }
}

resource sbSubscription 'Microsoft.ServiceBus/namespaces/topics/subscriptions@2022-10-01-preview' = {
  parent: sbTopic
  name: subscriptionName
  properties: {
    maxDeliveryCount: 10
    deadLetteringOnMessageExpiration: true
    lockDuration: 'PT5M'
    defaultMessageTimeToLive: 'P14D'
  }
}

Terraform equivalent: Use azurerm_servicebus_namespace with sku = "Premium" and zone_redundant = true, azurerm_servicebus_topic with requires_duplicate_detection = true and support_ordering = true, and azurerm_servicebus_subscription with max_delivery_count = 10 and dead_lettering_on_message_expiration = true.

Bicep — Event Grid Topic and Subscription with Dead-Letter Destination

This module provisions a custom Event Grid topic with CloudEvents 1.0 schema and public network access disabled, plus an event subscription that routes filtered events to a Service Bus queue and archives undeliverable events to Blob Storage.

param location string = resourceGroup().location
param topicName string
param serviceBusQueueId string
param deadLetterStorageAccountId string
param deadLetterContainerName string = 'event-grid-dlq'

resource eventGridTopic 'Microsoft.EventGrid/topics@2022-06-15' = {
  name: topicName
  location: location
  properties: {
    inputSchema: 'CloudEventSchemaV1_0'
    publicNetworkAccess: 'Disabled'
    disableLocalAuth: false
  }
}

resource eventSubscription 'Microsoft.EventGrid/topics/eventSubscriptions@2022-06-15' = {
  parent: eventGridTopic
  name: 'order-events-to-servicebus'
  properties: {
    destination: {
      endpointType: 'ServiceBusQueue'
      properties: {
        resourceId: serviceBusQueueId
      }
    }
    filter: {
      includedEventTypes: [
        'com.ascendion.orders.created'
        'com.ascendion.orders.updated'
      ]
    }
    retryPolicy: {
      maxDeliveryAttempts: 30
      eventTimeToLiveInMinutes: 1440
    }
    deadLetterDestination: {
      endpointType: 'StorageBlob'
      properties: {
        resourceId: deadLetterStorageAccountId
        blobContainerName: deadLetterContainerName
      }
    }
  }
}

Terraform equivalent: Use azurerm_eventgrid_topic with input_schema = "cloudeventschemaV1_0" and public_network_access_enabled = false, then azurerm_eventgrid_event_subscription with retry_policy block (max_delivery_attempts = 30, event_time_to_live = 1440) and a storage_blob_dead_letter_destination block pointing to the archive container.

Consumer Idempotency Pattern

Event Grid and Service Bus both provide at-least-once delivery. Track processed event IDs in Azure Table Storage or Cosmos DB before executing business logic. The conditional insert approach mirrors the DynamoDB pattern on AWS.

public async Task ProcessOrderEventAsync(CloudEvent cloudEvent)
{
    var eventId = cloudEvent.Id;

    // Idempotency check — conditional insert fails if already processed
    var entity = new TableEntity(partitionKey: "processed", rowKey: eventId)
    {
        ["processedAt"] = DateTimeOffset.UtcNow,
        ["expiresAt"]   = DateTimeOffset.UtcNow.AddDays(1)
    };

    try
    {
        await _tableClient.AddEntityAsync(entity); // Throws on duplicate key
    }
    catch (RequestFailedException ex) when (ex.Status == 409)
    {
        _logger.LogInformation("Event {EventId} already processed — skipping.", eventId);
        return;
    }

    await ExecuteOrderBusinessLogicAsync(cloudEvent);
}

Decision Criteria

Scenario Recommended Service Reason
State-change notifications, reactive integrations Event Grid Serverless per-operation pricing, CloudEvents schema, instant fan-out
Ordered command delivery, guaranteed processing Service Bus Premium Sessions, duplicate detection, dead-letter, zone redundancy
High-throughput telemetry, log aggregation Event Hubs Kafka protocol, 90-day retention, replay, partition-parallel consumers
Fan-out to multiple independent consumers Event Grid → Service Bus subscriptions Each consumer gets isolated subscription with own dead-letter
Ordered processing per entity Service Bus with session ID Groups all messages for an entity into a single ordered session
Cross-service domain event choreography Event Grid + Service Bus Event Grid routes; Service Bus delivers with guarantees
Real-time stream processing Event Hubs + Stream Analytics Native integration, windowed aggregation, no infrastructure
Replay of historical events Event Hubs Up to 90-day configurable retention with consumer-group offset reset

Cost Model

Service Pricing Unit Typical monthly cost at 10M events
Event Grid $0.60 per million operations (after 100k free) ~$6
Service Bus Premium Fixed $668/month per messaging unit (1 MU) ~$668 base + $0 per message
Service Bus Standard $0.013 per million operations ~$0.13 (not for production)
Event Hubs Standard $22.32/month per throughput unit + $0.028/GB ~$25–80 depending on volume
Event Hubs Premium Fixed per processing unit, higher retention ~$730/month per PU
Blob Storage (dead-letter archive) $0.018 per GB/month ~$1

Cost optimisation levers:
- Use Service Bus Standard only in non-production environments; Premium is required for sessions, zone redundancy, and private endpoints in production
- Size Event Hubs partitions at creation to match peak parallelism — you cannot reduce partition count later without recreating the namespace
- Enable Event Hubs auto-inflate (Standard/Premium) to scale throughput units automatically rather than over-provisioning
- Set message TTL to the minimum needed on Service Bus topics — retained messages count toward namespace storage quota
- Archive Event Hubs cold data to Azure Data Lake Storage Gen2 with Capture rather than extending retention periods

Anti-Patterns to Avoid

⚠ 1. Using Event Grid for High-Volume Telemetry

Routing 10,000+ events per second through Event Grid for telemetry or log aggregation, treating it as a general-purpose streaming bus. Event Grid is priced per operation and optimised for discrete domain events, not continuous high-throughput streams.

Hover to see the fix ↻
↺ Correct Approach

Use Azure Event Hubs for telemetry and log aggregation. Event Hubs supports the Kafka protocol, scales to millions of events per second, and provides up to 90-day retention with replay. Event Grid handles domain state-change notifications; Event Hubs handles data streams.

⚠ 2. Service Bus Standard Tier in Production

Deploying Service Bus Standard in a production workload that requires session-based ordered processing, private endpoints, or zone redundancy. Standard tier does not support any of these capabilities.

Hover to see the fix ↻
↺ Correct Approach

Use Service Bus Premium for all production deployments. Premium provides zone redundancy, private endpoints, session support, and Managed Identity enforcement. The fixed per-messaging-unit cost is predictable and the operational guarantees are non-negotiable for production.

⚠ 3. No Dead-Letter Monitoring

Creating Event Grid subscriptions or Service Bus topics with dead-letter destinations configured, but setting no Azure Monitor alert on the dead-letter count. Failed events accumulate silently until they expire or fill storage, with no signal to on-call engineers.

Hover to see the fix ↻
↺ Correct Approach

Create an Azure Monitor metric alert on DeadletteredMessageCount > 0 for every Service Bus subscription and a Log Analytics alert for Event Grid dead-letter blob writes. Treat every dead-lettered event as an incident requiring investigation.

⚠ 4. Shared Consumer Groups in Event Hubs

Multiple independent applications reading from the same Event Hubs consumer group. Consumer group offset is shared, so applications compete for messages, skip events intended for each other, and corrupt each other's offset tracking.

Hover to see the fix ↻
↺ Correct Approach

Create a dedicated consumer group per application. Event Hubs supports up to 20 consumer groups per namespace (Standard) or more on Premium. Each application maintains its own offset and can replay independently.

⚠ 5. No Duplicate Detection on Service Bus

Creating Service Bus topics without requiresDuplicateDetection = true when the producer may retry on network errors. A retry after a timeout can deliver the same command twice — placing the same order, charging the same payment — with no broker-side protection.

Hover to see the fix ↻
↺ Correct Approach

Enable requiresDuplicateDetection on Service Bus topics and ensure producers use a stable, idempotent MessageId (for example, a deterministic hash of the business key). The broker deduplicates within the configured history window (default 10 minutes).

Flowchart

%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','fontFamily':'IBM Plex Sans, system-ui, sans-serif','primaryColor':'#DBEAFE','primaryTextColor':'#1e3a5f','primaryBorderColor':'#2563EB','lineColor':'#374151','clusterBkg':'#F9FAFB','clusterBorder':'#D1D5DB','edgeLabelBackground':'#FFFFFF'},'flowchart':{'curve':'orthogonal','padding':30,'nodeSpacing':65,'rankSpacing':75,'useMaxWidth':true}}}%% flowchart TD START([Business Event Occurs in Producer Service]) START --> EG[Azure Event Grid\nCloudEvents 1.0 schema\nPublic network disabled] EG --> ROUTE{Subscription Filter\nincludedEventTypes match?} ROUTE -->|No match / undeliverable| BLOB[Azure Blob Storage\nDead-letter archive\nEvent Grid DLQ destination] ROUTE -->|Domain event| SB[Azure Service Bus Premium\nZone-redundant namespace\nManaged Identity only] ROUTE -->|Telemetry / log stream| EH[Azure Event Hubs\nKafka protocol\nRetention up to 90 days] SB --> SBSUB{Subscription Delivers\nUp to maxDeliveryCount: 10} SBSUB -->|Duplicate detected| SKIP[Skip — broker deduplication\nwithin history window] SBSUB -->|New command| PROCESS[Process Business Logic\nIdempotency check\nCosmosDB / Table Storage] SBSUB -->|MaxDelivery exceeded| SBDLQ[Service Bus Dead-Letter\nDeadletteredMessageCount alert\nManual replay via portal] EH --> EHPART[Partitions\nFixed at creation\nSized 2x peak parallelism] EHPART --> STREAM[Azure Stream Analytics\nor Kafka Consumer\nDedicated consumer group per app] PROCESS --> DONE([State Updated — Event Chain Complete]) style START fill:#4f8ef7,color:#fff style DONE fill:#10b981,color:#fff style SBDLQ fill:#fef3c7 style BLOB fill:#fef3c7 style SKIP fill:#fef3c7 style EG fill:#e0f2fe style EH fill:#d1fae5 style STREAM fill:#d1fae5

References

  1. Microsoft — Azure messaging services comparison. https://learn.microsoft.com/azure/service-bus-messaging/compare-messaging-services
  2. Microsoft — Event Grid delivery and retry. https://learn.microsoft.com/azure/event-grid/delivery-and-retry
  3. Microsoft — Azure Service Bus Premium messaging. https://learn.microsoft.com/azure/service-bus-messaging/service-bus-premium-messaging
  4. Microsoft — Azure Event Hubs features. https://learn.microsoft.com/azure/event-hubs/event-hubs-features
  5. Microsoft — CloudEvents schema support in Azure Event Grid. https://learn.microsoft.com/azure/event-grid/cloud-event-schema
  6. Microsoft — Azure Well-Architected Framework — Reliability. https://learn.microsoft.com/azure/well-architected/reliability/
  7. Portal: AWS event-driven comparison
Ascendion Engineering Knowledge Base ← Cloud