๐ฌ Part 2 — Azure Service Bus Topics & Subscriptions with .NET Microservices
In part 1 we built a Queue-based system:
- API → sends orders to a queue
- Worker → consumes and processes them
That works for point-to-point communication. But what if multiple services need to react to the same event? For example:
- Inventory Service → update stock
- Email Service → send confirmation email
- Billing Service → charge the customer
๐ That’s where Service Bus Topics & Subscriptions shine!
๐ Topics & Subscriptions: What They Are
- Topic → like a “broadcast station” where messages are published.
- Subscription → independent queue that receives a copy of each message.
- A single message → can fan out to many subscribers.
๐ก Each subscription can also apply filters to receive only certain messages.
๐️ Architecture
OrderApi (Producer) → OrdersTopic
↘ InventorySubscription → InventoryWorker
↘ EmailSubscription → EmailWorker
↘ BillingSubscription → BillingWorker
- OrderApi publishes messages into
orders-topic. - Each worker microservice listens on its own subscription.
- Subscriptions are independent → one failing worker doesn’t affect others.
⚙️ Step 1 — Create Topic & Subscriptions
You can do this in the Azure Portal or with CLI:
# Create a topic
az servicebus topic create -g myRg --namespace-name mysbnamespace -n orders-topic
# Create subscriptions
az servicebus subscription create -g myRg --namespace-name mysbnamespace \
--topic-name orders-topic -n inventory-subscription
az servicebus subscription create -g myRg --namespace-name mysbnamespace \
--topic-name orders-topic -n email-subscription
az servicebus subscription create -g myRg --namespace-name mysbnamespace \
--topic-name orders-topic -n billing-subscription
๐ค Step 2 — Producer (OrderApi) sends to Topic
Update OrderApi to publish messages to a topic instead of a queue.
using Azure.Messaging.ServiceBus;
public class ServiceBusSenderService : IAsyncDisposable
{
private readonly ServiceBusClient _client;
private readonly ServiceBusSender _sender;
public ServiceBusSenderService(IConfiguration config)
{
_client = new ServiceBusClient(config["ServiceBus:ConnectionString"]);
_sender = _client.CreateSender(config["ServiceBus:TopicName"]); // topic instead of queue
}
public async Task SendOrderMessageAsync(Order order)
{
var json = System.Text.Json.JsonSerializer.Serialize(order);
var message = new ServiceBusMessage(json)
{
ContentType = "application/json",
Subject = "OrderPlaced",
MessageId = Guid.NewGuid().ToString()
};
await _sender.SendMessageAsync(message);
}
public async ValueTask DisposeAsync()
{
await _sender.DisposeAsync();
await _client.DisposeAsync();
}
}
appsettings.json
"ServiceBus": {
"ConnectionString": "<YOUR_SERVICEBUS_CONNECTION_STRING>",
"TopicName": "orders-topic"
}
๐ฅ Step 3 — Consumers (Subscribers)
Each microservice connects to the same topic but listens to its own subscription.
Inventory Worker
var processor = client.CreateProcessor("orders-topic", "inventory-subscription");
processor.ProcessMessageAsync += async args =>
{
var body = args.Message.Body.ToString();
Console.WriteLine($"[Inventory] Updating stock for: {body}");
await args.CompleteMessageAsync(args.Message);
};
processor.ProcessErrorAsync += args =>
{
Console.WriteLine($"Inventory error: {args.Exception}");
return Task.CompletedTask;
};
await processor.StartProcessingAsync();
Email Worker
var processor = client.CreateProcessor("orders-topic", "email-subscription");
processor.ProcessMessageAsync += async args =>
{
var body = args.Message.Body.ToString();
Console.WriteLine($"[Email] Sending confirmation: {body}");
await args.CompleteMessageAsync(args.Message);
};
await processor.StartProcessingAsync();
Billing Worker
var processor = client.CreateProcessor("orders-topic", "billing-subscription");
processor.ProcessMessageAsync += async args =>
{
var body = args.Message.Body.ToString();
Console.WriteLine($"[Billing] Charging payment for: {body}");
await args.CompleteMessageAsync(args.Message);
};
await processor.StartProcessingAsync();
๐️ Step 4 — Subscription Filters
You can configure subscriptions to only receive specific messages.
Example: Only route messages with Product = "Laptop" to premium-subscription.
az servicebus subscription rule create \
-g myRg --namespace-name mysbnamespace --topic-name orders-topic \
--subscription-name premium-subscription -n LaptopOnlyRule \
--filter-sql-expression "Product = 'Laptop'"
Now, premium-subscription only gets Laptop orders, ignoring everything else.
๐งช Step 5 — Test It
- Run
OrderApiand POST an order:
curl -X POST http://localhost:5000/api/orders \
-H "Content-Type: application/json" \
-d '{"id":1,"product":"Laptop","quantity":1,"price":999.99}'
- Logs:
[Inventory] Updating stock for: {"Id":1,"Product":"Laptop",...}
[Email] Sending confirmation: {"Id":1,"Product":"Laptop",...}
[Billing] Charging payment for: {"Id":1,"Product":"Laptop",...}
- If you send
"Mouse"instead, the premium-subscription will not receive it (due to filter).
๐ Use Cases for Topics
- Event-driven systems: One event → many subscribers.
- Fan-out architecture: Notify multiple services without tight coupling.
- Selective routing: Use filters to send only relevant events to subscribers.
- Auditing: Create a subscription just for logging/monitoring purposes.
๐ Best Practices
- ✅ Use filters to avoid unnecessary message processing.
- ✅ Always handle DLQs for each subscription.
- ✅ Scale consumers independently — one slow consumer won’t affect others.
- ✅ Make consumers idempotent (handle duplicate messages).
- ✅ Secure with Managed Identity + RBAC instead of connection strings in production.
๐ฏ Conclusion
In this part, we extended our microservices system with Topics & Subscriptions:
- One producer publishes to a topic
- Multiple consumers process the same message independently
- Filters help with selective message routing
This pattern is the backbone of event-driven microservices on Azure.
๐ In Part 3, we’ll dive into:
- Monitoring with Application Insights
- Dead Letter Queue (DLQ) handling
- Retry strategies and exponential backoff
Comments
Post a Comment