In Microservices Architecture, services need to communicate with each other. The simplest approach is synchronous HTTP calls, but it creates tight coupling between services. If one service is slow or unavailable, it affects all dependent services.
Message brokers solve this problem by enabling asynchronous, message-based communication between your services. Services publish events to the broker, and other services consume them independently.
When you are working in Azure, you can use Azure Service Bus to implement a message broker. It supports both point-to-point and publish-subscribe messaging patterns.
In this post, we will explore:
- Azure Service Bus: Core Concepts
- How To Create Azure Service Bus in Azure Portal
- Setting Up Azure Service Bus with .NET Aspire
- How to Publish and Consume Messages with Azure Service Bus
- The Event-Driven Pipeline: Payments, Fraud Detection, and Notifications
- Running the Application Locally with Azure Service Bus Emulator
- Deploying Microservices to Azure with Aspire
- Azure Service Bus vs Other Azure Messaging Services
Let's dive in.
Azure Service Bus: Core Concepts
Azure Service Bus is a fully managed enterprise message broker in the Azure Cloud. It supports two messaging patterns: Queues and Topics with Subscriptions.
Queues implement point-to-point communication. One sender sends a message, and one receiver processes it. Each message is consumed by exactly one consumer.
Use queues when a message should be processed by a single service. For example, sending a stock update command to the Stocks service.
Topics with Subscriptions
Topics with Subscriptions implement publish-subscribe communication. One sender publishes a message to a topic, and multiple subscribers can each receive a copy of that message. Each subscription acts as its own virtual queue.
Use topics when multiple services need to react to the same event. For example, when a payment is processed, both the Fraud Detection service and the Notifications service may need to be notified.
Our solution has five microservices that communicate through Azure Service Bus:
- Product-Service - manages products and handles purchases
- Stock-Service - manages product stock levels
- Payment-Service - processes payments and tracks their status
- FraudDetection-Service - analyzes payments for fraud risk
- Notification-Service - sends email notifications
Here is the message flow:

When a user purchases a product:
- Product-Service publishes
UpdateStockEventto theupdate-stockqueue andPurchaseCompletedEventto thepayment-createdqueue - Stock-Service consumes from
update-stockand updates the stock count - Payment-Service consumes from
payment-created, creates a payment record, and publishesPaymentRegisteredEventto thepayment-registeredtopic - FraudDetection-Service subscribes to the
payment-registeredtopic, analyzes the payment, and publishesFraudDecisionEventto thefraud-decisionqueue - Payment-Service consumes from
fraud-decision, updates the payment status, and publishesPaymentProcessedEventto thepayment-processedtopic - Notification-Service subscribes to the
payment-processedtopic and sends a notification
In our project, we use both:
- Queues for
update-stock,payment-created, andfraud-decision(one sender, one receiver) - Topics for
payment-registeredandpayment-processed(one sender, multiple receivers)
How Messages Flow Through Service Bus
When a consumer receives a message, Service Bus does not delete it immediately. It locks the message and hides it from other consumers for a configurable lock duration.
The consumer has that time to process the message and complete the call. If it completes successfully, Service Bus deletes the message. If the handler throws or the lock expires, Service Bus releases the message and makes it available for retry.
Service Bus tracks how many times a message has been delivered using the delivery count.
When the delivery count exceeds MaxDeliveryCount (default: 10), Service Bus automatically moves the message to the dead-letter queue.
At-least-once delivery is the core guarantee Service Bus provides. A message will be delivered at least once, but in rare cases it may be delivered more than once — for example, if a network failure prevents the completion acknowledgement from reaching the Service Bus. Your consumers must be idempotent: processing the same message twice must produce the same result.
Every queue and every topic subscription has a built-in dead-letter sub-queue (DLQ). Messages end up in the DLQ when:
- The delivery count exceeds
MaxDeliveryCount - Your code explicitly calls
DeadLetterMessageAsync— for example, when a message is malformed, and retrying is pointless - The message TTL (time-to-live) expires before it is consumed
The DLQ is permanent — messages do not expire there. You can inspect, replay, or archive them using the Azure Portal.
In production, monitor the DLQ message count and set up alerts. A growing DLQ means something in your pipeline is consistently failing.
Service Bus can automatically discard duplicate messages within a configurable time window. Enable it on a queue or topic at creation time.
Service Bus tracks messages by their MessageId.
If a message with the same MessageId arrives within the duplicate detection window (default: 10 minutes), Service Bus silently drops it.
Use duplicate detection when your producer may retry sending — for example, after a timeout or a transient network failure.
Sessions enable ordered, grouped processing of related messages.
Set a SessionId on each message to group them together.
Service Bus guarantees that all messages with the same SessionId are processed in order by a single consumer at a time.
No two consumers can process messages from the same session simultaneously.
Use sessions when order matters — for example, processing all state transitions for the same payment in sequence.
Sessions require RequiresSession = true on the queue or subscription.
How To Create Azure Service Bus in Azure Portal
First, create a new Azure Service Bus namespace in the Azure Portal.
Go to the Azure Portal and search for "Service Bus":

Create a new Service Bus namespace with the following settings:
- Resource group: Choose your resource group or create a new one
- Namespace name: Choose a globally unique name
- Location: Select the region closest to your services
- Pricing tier: Select Standard

The pricing tier is important. The Basic tier only supports queues. If you need topics and subscriptions (which we do), you must use at least the Standard tier.
For most development and production workloads, Standard is sufficient. Choose Premium only if you need dedicated resources, virtual network integration, or messages larger than 256 KB.
Setting Up Azure Service Bus with .NET Aspire
We use .NET Aspire to orchestrate all services and their dependencies.
To get started with Aspire, read this article
Install the following NuGet packages in the Aspire AppHost project:
1dotnet add package Aspire.Hosting.Azure.ServiceBus 2dotnet add package Aspire.Hosting.PostgreSQL 3dotnet add package Aspire.Hosting.Redis
Here is the full AppHost.cs that wires everything together:
1var builder = DistributedApplication.CreateBuilder(args); 2 3var postgres = builder.AddPostgres("postgres"); 4var redis = builder.AddRedis("cache"); 5 6var serviceBus = builder.AddAzureServiceBus("service-bus") 7 .RunAsEmulator(); 8 9serviceBus.AddServiceBusQueue("update-stock"); 10serviceBus.AddServiceBusQueue("payment-created"); 11serviceBus.AddServiceBusQueue("fraud-decision"); 12 13var paymentRegisteredTopic = serviceBus.AddServiceBusTopic("payment-registered"); 14paymentRegisteredTopic.AddServiceBusSubscription("fraud-detection"); 15 16var paymentProcessedTopic = serviceBus.AddServiceBusTopic("payment-processed"); 17paymentProcessedTopic.AddServiceBusSubscription("notifications"); 18 19var stocksApi = builder.AddProject<Projects.Stocks_Api>("stocks-api") 20 .WithReference(postgres) 21 .WithReference(serviceBus) 22 .WithExternalHttpEndpoints(); 23 24builder.AddProject<Projects.Products_Api>("products-api") 25 .WithReference(stocksApi) 26 .WithReference(postgres) 27 .WithReference(redis) 28 .WithReference(serviceBus) 29 .WithExternalHttpEndpoints(); 30 31builder.AddProject<Projects.Payments_Api>("payments-api") 32 .WithReference(postgres) 33 .WithReference(serviceBus) 34 .WithExternalHttpEndpoints(); 35 36builder.AddProject<Projects.FraudDetection_Api>("fraud-detection-api") 37 .WithReference(serviceBus) 38 .WithExternalHttpEndpoints(); 39 40builder.AddProject<Projects.Notifications_Api>("notifications-api") 41 .WithReference(serviceBus) 42 .WithExternalHttpEndpoints(); 43 44builder.Build().Run();
The RunAsEmulator() method tells Aspire to run a local Azure Service Bus emulator in Docker.
This means you do not need an Azure subscription to develop locally.
Aspire automatically injects the Service Bus connection string into each service via WithReference(serviceBus).
For queues, we use AddServiceBusQueue.
For topics, we use AddServiceBusTopic followed by AddServiceBusSubscription to create subscriptions on that topic.
Each service project needs the Aspire Azure Service Bus client package:
1dotnet add package Aspire.Azure.Messaging.ServiceBus
And register the client in Program.cs:
1builder.AddAzureServiceBusClient("service-bus");
The "service-bus" string must match the name used in the AppHost.
To enable distributed traces for Azure Service Bus, add the following line in Program.cs of every service that uses Service Bus:
1AppContext.SetSwitch("Azure.Experimental.EnableActivitySource", true);
This opt-in switch activates the Azure SDK's built-in ActivitySource, which emits OpenTelemetry spans for every send and receive operation.
Without it, the Aspire dashboard Traces tab will not show Service Bus activity, and you will not be able to see distributed traces across your services.
You also need to register the Azure Service Bus activity source in the Aspire ServiceDefaults project.
Open Extensions.cs and add .AddSource("Azure.Messaging.ServiceBus") to the OpenTelemetry tracing configuration:
1.WithTracing(tracing => tracing 2 .AddAspNetCoreInstrumentation() 3 // ... other sources 4 .AddSource("Azure.Messaging.ServiceBus"));
This tells OpenTelemetry to collect and export spans produced by the Azure SDK. After both changes, the Aspire dashboard will show end-to-end distributed traces across all five services.
All events are defined as C# records in a shared Shared.Contracts project.
This project has no dependencies and is referenced by all services.
Here are a few event contracts:
1public record UpdateStockEvent( 2 int ProductId, 3 int CountChange); 4 5public record PurchaseCompletedEvent( 6 string TransactionId, 7 int ProductId, 8 string ProductName, 9 int Quantity, 10 decimal TotalPrice, 11 string UserId, 12 DateTime Timestamp);
UpdateStockEvent carries the product ID and the quantity change (negative for purchases).
PurchaseCompletedEvent carries the full purchase details needed to create a payment.
Other events in the project follow the same pattern: PaymentRegisteredEvent, FraudDecisionEvent, and PaymentProcessedEvent.
How to Publish and Consume Messages with Azure Service Bus
To avoid duplicating Service Bus code across services, we create reusable abstractions in a Shared.ServiceBus project.
This project depends on the Azure.Messaging.ServiceBus NuGet package:
1dotnet add package Azure.Messaging.ServiceBus
We define a simple interface:
1public interface IServiceBusPublisher 2{ 3 Task PublishAsync<T>(string queueOrTopic, T message, CancellationToken cancellationToken = default); 4}
The publisher accepts a queue or topic name and any message type. Here is the implementation:
1public class ServiceBusPublisher(ServiceBusClient client) : IServiceBusPublisher, IAsyncDisposable 2{ 3 private readonly ConcurrentDictionary<string, ServiceBusSender> _senders = new(); 4 5 public async Task PublishAsync<T>(string queueOrTopic, T message, 6 CancellationToken cancellationToken = default) 7 { 8 var sender = _senders.GetOrAdd(queueOrTopic, client.CreateSender); 9 10 var json = JsonSerializer.Serialize(message); 11 var serviceBusMessage = new ServiceBusMessage(json) 12 { 13 ContentType = "application/json", 14 MessageId = Guid.NewGuid().ToString() 15 }; 16 17 await sender.SendMessageAsync(serviceBusMessage, cancellationToken); 18 } 19 20 public async ValueTask DisposeAsync() 21 { 22 foreach (var sender in _senders.Values) 23 { 24 await sender.DisposeAsync(); 25 } 26 27 _senders.Clear(); 28 } 29}
Creating a ServiceBusSender for every message is expensive.
We cache senders by queue/topic name so that each destination gets one reusable sender.
ConcurrentDictionary.GetOrAdd ensures thread safety when multiple threads publish to the same destination.
We set a unique MessageId on each message.
Service Bus uses this for duplicate detection — if a message with the same MessageId arrives within the duplicate detection window, Service Bus silently drops it.
The publisher is registered as a singleton because ServiceBusSender is thread-safe and designed for reuse:
1services.AddSingleton<IServiceBusPublisher, ServiceBusPublisher>();
Here is how Product-Service publishes events when a user purchases a product:
1app.MapPost("/products/{id:int}/purchase", async ( 2 int id, 3 PurchaseProductRequest request, 4 ProductService productService, 5 IStocksApiClient stocksApiClient, 6 IServiceBusPublisher serviceBusPublisher) => 7{ 8 var product = await productService.GetByIdAsync(id); 9 if (product is null) 10 { 11 return Results.NotFound($"Product with ID {id} not found."); 12 } 13 14 var hasEnoughStock = await stocksApiClient.HasEnoughStockAsync(id, request.Quantity); 15 if (!hasEnoughStock) 16 { 17 return Results.Conflict("Not enough stock for this purchase."); 18 } 19 20 var totalPrice = product.Price * request.Quantity; 21 22 // Publish stock update event (async via Service Bus) 23 await serviceBusPublisher.PublishAsync("update-stock", 24 new UpdateStockEvent(id, -request.Quantity)); 25 26 // Publish purchase completed event for payment processing 27 await serviceBusPublisher.PublishAsync("payment-created", 28 new PurchaseCompletedEvent( 29 TransactionId: Guid.NewGuid().ToString(), 30 ProductId: product.Id, 31 ProductName: product.Name, 32 Quantity: request.Quantity, 33 TotalPrice: totalPrice, 34 UserId: "user-1", 35 Timestamp: DateTime.UtcNow)); 36 37 return Results.Ok(new PurchaseProductResponse( 38 ProductId: product.Id, 39 Quantity: request.Quantity, 40 TotalPrice: totalPrice)); 41});
Notice that the stock check is still synchronous via HTTP. We need an immediate answer to know if the purchase can proceed.
But the stock update is now asynchronous.
We publish an UpdateStockEvent to the update-stock queue and return immediately.
The Stocks service will process it in the background.
First, we define a handler interface that all message handlers implement:
1public interface IServiceBusMessageHandler<in TMessage> 2{ 3 Task HandleAsync(TMessage message, CancellationToken cancellationToken); 4}
Next, we create a generic consumer BackgroundService that wraps ServiceBusProcessor from the Azure SDK.
It deserializes incoming messages and dispatches them to the corresponding IServiceBusMessageHandler<T>:
1public class ServiceBusQueueConsumerService<TMessage>( 2 ServiceBusClient client, 3 IServiceScopeFactory scopeFactory, 4 ILogger<ServiceBusQueueConsumerService<TMessage>> logger, 5 string queueName) : BackgroundService 6{ 7 protected override async Task ExecuteAsync(CancellationToken stoppingToken) 8 { 9 var processor = client.CreateProcessor(queueName, new ServiceBusProcessorOptions 10 { 11 AutoCompleteMessages = true, 12 MaxConcurrentCalls = 10 13 }); 14 15 processor.ProcessMessageAsync += async args => 16 { 17 try 18 { 19 var message = JsonSerializer.Deserialize<TMessage>(args.Message.Body.ToString()); 20 if (message is not null) 21 { 22 using var scope = scopeFactory.CreateScope(); 23 var handler = scope.ServiceProvider.GetRequiredService<IServiceBusMessageHandler<TMessage>>(); 24 await handler.HandleAsync(message, stoppingToken); 25 } 26 } 27 catch (Exception ex) 28 { 29 logger.LogError(ex, "Error processing message from queue {QueueName}", queueName); 30 throw; 31 } 32 }; 33 34 processor.ProcessErrorAsync += args => 35 { 36 logger.LogError(args.Exception, "Error in Service Bus processor for queue {QueueName}", queueName); 37 return Task.CompletedTask; 38 }; 39 40 await processor.StartProcessingAsync(stoppingToken); 41 42 // Keep the service alive while the processor is running. 43 while (!stoppingToken.IsCancellationRequested) {} 44 45 await processor.StopProcessingAsync(stoppingToken); 46 await processor.DisposeAsync(); 47 } 48}
ServiceBusProcessor** is the Azure SDK's built-in message pump. It pulls messages from the queue and calls your handler.
We use the following options:
AutoCompleteMessages = true- tells SDK to automatically mark messages as completed after your handler finishes without throwing. If your handler throws an exception, the message becomes available for retry.MaxConcurrentCalls = 10- allows processing up to 10 messages in parallel for better throughput.
The while loop at the end keeps the BackgroundService alive while the processor runs.
We register the handler and the consumer service in the DI container:
1services.AddSingleton<IServiceBusPublisher, ServiceBusPublisher>(); 2 3services.AddScoped<IServiceBusMessageHandler<UpdateStockEvent>, UpdateStockMessageHandler>(); 4services.AddHostedService(sp => new ServiceBusQueueConsumerService<UpdateStockEvent>( 5 sp.GetRequiredService<ServiceBusClient>(), 6 sp.GetRequiredService<IServiceScopeFactory>(), 7 sp.GetRequiredService<ILogger<ServiceBusQueueConsumerService<UpdateStockEvent>>>(), 8 "update-stock"));
The handler is registered as scoped, so it gets a fresh DbContext and other scoped dependencies per message.
The consumer service is registered via a factory lambda that passes the queue name.
For consuming messages from a topic, we use a different consumer class, which is identical to this one, except for how it creates a processor:
1var processor = client.CreateProcessor(topicName, subscriptionName, new ServiceBusProcessorOptions 2{ 3 AutoCompleteMessages = true, 4 MaxConcurrentCalls = 10 5});
The Event-Driven Pipeline: Payments, Fraud Detection, and Notifications
Now let's explore the most interesting part of our architecture: the payment processing pipeline that flows through four services.
Here is the complete message flow:

Payment-Service - Creating Payments
When a purchase happens, Payment-Service consumes the PurchaseCompletedEvent from the payment-created queue:
1public class PurchaseCompletedMessageHandler( 2 IPaymentRepository paymentRepository, 3 IServiceBusPublisher serviceBusPublisher, 4 ILogger<PurchaseCompletedMessageHandler> logger) 5 : IServiceBusMessageHandler<PurchaseCompletedEvent> 6{ 7 public async Task HandleAsync(PurchaseCompletedEvent message, 8 CancellationToken cancellationToken) 9 { 10 var payment = new Payment(...); 11 12 await paymentRepository.AddAsync(payment); 13 14 var paymentRegisteredEvent = new PaymentRegisteredEvent(...); 15 16 await serviceBusPublisher.PublishAsync("payment-registered", paymentRegisteredEvent, cancellationToken); 17 18 logger.LogInformation("Payment {TransactionId} created and registered", payment.TransactionId); 19 } 20}
This handler does two things:
- Creates a
Paymententity in the database with statusProcessing - Publishes a
PaymentRegisteredEventto thepayment-registeredtopic
We publish to a topic here and the Fraud Detection service subscribes to it. In the future, you could add more subscribers (analytics, audit logging) without changing the Payments service.
FraudDetection-Service - Analyzing Payments
The Fraud Detection service subscribes to the payment-registered topic:
1public class PaymentRegisteredMessageHandler( 2 IFraudDetectionEngine fraudDetectionEngine, 3 IServiceBusPublisher serviceBusPublisher, 4 ILogger<PaymentRegisteredMessageHandler> logger) 5 : IServiceBusMessageHandler<PaymentRegisteredEvent> 6{ 7 public async Task HandleAsync(PaymentRegisteredEvent message, 8 CancellationToken cancellationToken) 9 { 10 var fraudDecision = await fraudDetectionEngine.AnalyzePaymentAsync(message, cancellationToken); 11 12 await serviceBusPublisher.PublishAsync("fraud-decision", fraudDecision, cancellationToken); 13 14 logger.LogInformation( 15 "Fraud analysis completed for transaction {TransactionId}: Decision={Decision}", 16 message.TransactionId, fraudDecision.Decision); 17 } 18}
This handler subscribes to the payment-registered topic via ServiceBusTopicConsumerService (registered in DI).
The topic name and subscription name are specified during DI registration.
The fraud detection engine evaluates risk factors like high-risk countries, suspicious amounts, late-night transactions, and high-risk card BINs. Each factor contributes a weighted score, and the total determines the decision:
- Allow (score below 100): Payment is safe
- Review (score 100-699): Payment needs manual review
- Block (score 700+): Payment is rejected
The fraud decision is published to the fraud-decision queue, which goes back to Payment-Service.
Payment-Service - Processing Fraud Decisions
When the fraud decision arrives, Payment-Service updates the payment status:
1public class FraudDecisionMessageHandler( 2 IPaymentRepository paymentRepository, 3 IServiceBusPublisher serviceBusPublisher, 4 ILogger<FraudDecisionMessageHandler> logger) 5 : IServiceBusMessageHandler<FraudDecisionEvent> 6{ 7 public async Task HandleAsync(FraudDecisionEvent message, CancellationToken cancellationToken) 8 { 9 var paymentStatus = message.Decision switch 10 { 11 "Allow" => PaymentStatus.Confirmed, 12 "Review" => PaymentStatus.Reviewing, 13 "Block" => PaymentStatus.Rejected, 14 _ => PaymentStatus.Reviewing 15 }; 16 17 var payment = await paymentRepository.GetByTransactionIdAsync(message.TransactionId); 18 if (payment is null) 19 { 20 logger.LogWarning("Payment {TransactionId} not found for fraud decision", message.TransactionId); 21 return; 22 } 23 24 payment.Status = paymentStatus; 25 await paymentRepository.UpdateAsync(payment); 26 27 var paymentProcessedEvent = new PaymentProcessedEvent( 28 payment.TransactionId, 29 payment.Status.ToString(), 30 DateTime.UtcNow); 31 32 await serviceBusPublisher.PublishAsync("payment-processed", paymentProcessedEvent, cancellationToken); 33 34 logger.LogInformation( 35 "Payment {TransactionId} processed with status {Status}", 36 payment.TransactionId, payment.Status); 37 } 38}
The handler maps the fraud decision to a payment status using a switch expression.
After updating the database, it publishes a PaymentProcessedEvent to the payment-processed topic.
The final step in the pipeline:
Notification-Service subscribes to the payment-processed topic and sends an email notification.
Running the Application Locally with the Emulator
The Azure Service Bus emulator is a Docker container that runs a local instance of Service Bus. This means you do not need an Azure subscription to develop and test locally.
To run the application, set the AppHost project as the startup project and press F5.
Aspire will:
- Start the PostgreSQL container
- Start the Redis container
- Start the Azure Service Bus emulator container
- Create all queues, topics, and subscriptions defined in
AppHost.cs - Start all five API services with injected connection strings
- Open the Aspire dashboard in your browser
The first run may take a few minutes while Docker pulls the container images. Subsequent runs will be much faster.
In the Aspire dashboard, you can view:
- Resources - All running services and their health status
- Console logs - Real-time logs from each service
- Structured logs - Filterable and searchable logs
- Traces - Distributed traces across services
Here is what our Aspire dashboard looks like:

Here, you can see all the queues and topics created by the AppHost.
To test the full pipeline, call the purchase endpoint on Product-Service:
1POST /products/{id}/purchase 2{ 3 "quantity": 1 4}
Then watch the logs in the Aspire dashboard. You will see messages flowing in distributed traces through all five services in sequence:

Deploying Microservices to Azure with Aspire
For production, you need to switch from the emulator to a real Azure Service Bus.
Aspire supports deploying to Azure Container Apps using the Azure Developer CLI (azd).
First, install the Azure Developer CLI:
1winget install microsoft.azd
Then run the following commands from the project's solution folder:
1azd auth login 2azd init
azd auth login authenticates with Azure and downloads the Azure Developer CLI configuration.
You only need to run this once.
azd init initializes the Azure deployment configuration.
It scans your solution and generates Bicep infrastructure-as-code templates for all resources.
When deploying to Azure, we need a different configuration for AzureServiceBus with the RunAsEmulator() call removed:
1if (builder.Environment.IsProduction()) 2{ 3 // Existing Azure Service Bus: queues and topics already exist 4 var serviceBus = builder.AddConnectionString("service-bus"); 5 6 stocksApi.WithReference(serviceBus); 7 productsApi.WithReference(serviceBus); 8 paymentsApi.WithReference(serviceBus); 9 fraudDetectionApi.WithReference(serviceBus); 10 notificationsApi.WithReference(serviceBus); 11}
If we use an existing Service Bus, we can just use the AddConnectionString call and pass the connection string to the WithReference method to all services.
For this to work, we need to create queues and topics on the existing Azure Service Bus via Azure Portal or the Azure CLI:
First, add the queues:
- payment-created
- update-stock
- fraud-decision

Second, create the topics:
- payment-registered
- payment-processed

Third, create subscriptions for the topics:
- payment-registered -> fraud-detection
- payment-processed -> notifications

Finally, run the azd up command to provision Azure resources and deploy your application.
1azd up
Aspire will create:
- Azure Container Apps for each of your five services
- Azure Database for PostgreSQL flexible server
- Azure Cache for Redis
- Azure Container Registry to store Docker images
All connection strings and service discovery are wired automatically, just as they would be locally.
Let's test the full pipeline by calling the purchase endpoint on the purchase API endpoint:

If you have created an Azure Service Bus in the Azure portal, you need to use the AddConnectionString to connect all services to it.
You can also AddConnectionString to connect to an existing Service Bus when running your services locally (if public access is enabled for this service).
If you want Aspire to create the Service Bus for you, use the AddAzureServiceBus as you have seen earlier.
Azure Service Bus vs Other Azure Messaging Services
Azure offers three messaging services. They are often confused because their names sound similar.
Azure Service Bus is an enterprise message broker with guaranteed delivery, dead-letter queue, sessions, and ordering. Use it for reliable service-to-service communication — commands, events, and workflows between microservices. This is what we use in this post.
Azure Event Hubs is a high-throughput event streaming platform built for millions of events per second. It has no dead-letter queue and no per-message acknowledgement. Use it for telemetry pipelines, log ingestion, and analytics workloads.
Azure Event Grid is a reactive eventing service for Azure infrastructure events. It is push-based and serverless. Use it to react to Azure resource changes — for example, when a blob is uploaded to Storage or a VM is deallocated.
If you need reliable delivery with retries and a dead-letter queue → use Service Bus. If you need massive-throughput event streaming → use Event Hubs. If you need to react to Azure resource events → use Event Grid.
Hope you find this newsletter useful. See you next time.