Appearance
Azure Service Bus
Azure Service Bus (ASB) is the asynchronous messaging backbone for Microtec ERP. It replaced RabbitMQ as the production message broker following a migration decision (ADR-005). ASB is used by the Notification, Workflow, and Worker services for reliable event-driven communication between microservices.
Background: RabbitMQ Migration
Why ASB over RabbitMQ?
RabbitMQ was used in early development (on-prem and dev environments). The migration to ASB was driven by:
- Managed service: no RabbitMQ VM to maintain or patch
- Native Azure integration: KEDA scaling triggers, managed identity auth
- Dead-letter queue management via Azure Portal
- Higher durability guarantees (99.9% SLA)
RabbitMQ is still available in dev as a local Docker container but is not used in any cloud environment.
Namespace Inventory
| Environment | Namespace | SKU | Resource Group |
|---|---|---|---|
| dev | mic-erp-be-dev-asb | Standard | mic-erp-be-dev-data-rg |
| stage | mic-erp-be-stage-asb | Standard | mic-erp-be-stage-data-rg |
| preprod | mic-erp-be-preprod-asb | Standard | mic-erp-be-preprod-data-rg |
| uat | mic-erp-be-uat-asb | Standard | mic-erp-be-uat-data-rg |
| production | mic-erp-be-prod-asb | Premium | mic-erp-be-prod-data-rg |
Production uses the Premium SKU for VNet integration (private endpoint, no public internet exposure) and 10 GB per message unit (vs 256 KB in Standard).
Topics and Subscriptions
All messaging uses the topic/subscription pattern (pub-sub), not queues. This allows multiple consumers to independently process the same event.
Topic Inventory
| Topic | Published By | Description |
|---|---|---|
invoices | Accounting | Invoice created, approved, reversed |
vouchers | Accounting | Journal voucher posted |
hr-events | HR | Employee onboarding, leave requests, payroll |
workflow-events | Workflow | Step completed, approval required, escalation |
notifications | All services | Notification dispatch requests |
tenant-events | BusinessOwner | Tenant created, suspended, deleted |
import-events | Import service | Import job status updates |
Subscription Configuration
bicep
// For each topic-subscription pair
resource notificationInvoiceSub 'Microsoft.ServiceBus/namespaces/topics/subscriptions@2022-10-01-preview' = {
name: 'notification-invoice'
parent: invoicesTopic
properties: {
maxDeliveryCount: 10 // Retry before dead-letter
lockDuration: 'PT2M' // 2-minute processing window
deadLetteringOnMessageExpiration: true
defaultMessageTimeToLive: 'P7D' // 7-day message TTL
enableDeadLetteringOnFilterEvaluationExceptions: true
}
}Connection String and Authentication
Connection String (Standard SKU)
The connection string is stored in Key Vault:
Endpoint=sb://mic-erp-be-{env}-asb.servicebus.windows.net/;
SharedAccessKeyName=RootManageSharedAccessKey;
SharedAccessKey={from-kv}Key Vault secret: AzureServiceBus--ConnectionString
Managed Identity (Premium / Production)
Production uses managed identity instead of shared access keys, eliminating the need for a connection string secret:
csharp
// Program.cs (production configuration)
services.AddAzureServiceBus(options =>
{
options.FullyQualifiedNamespace =
"mic-erp-be-prod-asb.servicebus.windows.net";
// No connection string — uses DefaultAzureCredential
});The Container App's managed identity must have the Azure Service Bus Data Owner role on the namespace.
Producer Pattern
csharp
// EventPublisher.cs
public class EventPublisher : IEventPublisher
{
private readonly ServiceBusSender _sender;
public async Task PublishAsync<T>(T @event, CancellationToken ct = default)
where T : IDomainEvent
{
var topicName = TopicRegistry.GetTopicName<T>();
var body = JsonSerializer.SerializeToUtf8Bytes(@event);
var message = new ServiceBusMessage(body)
{
MessageId = Guid.NewGuid().ToString(),
Subject = @event.GetType().Name,
ContentType = "application/json",
CorrelationId = Activity.Current?.TraceId.ToString(),
ApplicationProperties =
{
["TenantId"] = @event.TenantId,
["EventType"] = @event.GetType().Name,
["Version"] = "1.0"
}
};
await _sender.SendMessageAsync(message, ct);
}
}Consumer Pattern
csharp
// NotificationConsumer.cs (Notification service)
public class InvoiceNotificationConsumer : IHostedService
{
public async Task StartAsync(CancellationToken ct)
{
_processor = _client.CreateProcessor(
topicName: "invoices",
subscriptionName: "notification-invoice",
new ServiceBusProcessorOptions
{
MaxConcurrentCalls = 5,
AutoCompleteMessages = false,
MaxAutoLockRenewalDuration = TimeSpan.FromMinutes(5)
}
);
_processor.ProcessMessageAsync += HandleMessageAsync;
_processor.ProcessErrorAsync += HandleErrorAsync;
await _processor.StartProcessingAsync(ct);
}
private async Task HandleMessageAsync(ProcessMessageEventArgs args)
{
try
{
var invoice = JsonSerializer.Deserialize<InvoiceCreatedEvent>(
args.Message.Body
);
await _notificationService.SendInvoiceNotificationAsync(invoice!);
await args.CompleteMessageAsync(args.Message);
}
catch (Exception ex)
{
// After maxDeliveryCount retries, message goes to dead-letter queue
await args.AbandonMessageAsync(args.Message);
_logger.LogError(ex, "Failed to process invoice notification");
}
}
}KEDA Scaling Trigger
The Notification service scales based on the number of pending messages in its subscription:
bicep
{
name: 'servicebus-notifications'
custom: {
type: 'azure-servicebus'
metadata: {
topicName: 'notifications'
subscriptionName: 'notification-processor'
messageCount: '10' // Scale up when >10 messages pending
namespace: 'mic-erp-be-{env}-asb.servicebus.windows.net'
}
auth: [
{
secretRef: 'servicebus-connection'
triggerParameter: 'connection'
}
]
}
}Dead Letter Queue Management
Messages that fail maxDeliveryCount retries are moved to the dead-letter queue (DLQ). Review DLQ messages regularly:
bash
# List DLQ messages via Azure CLI
az servicebus topic subscription message peek \
--resource-group mic-erp-be-dev-data-rg \
--namespace-name mic-erp-be-dev-asb \
--topic-name invoices \
--subscription-name notification-invoice \
--message-count 10 \
--subqueue deadletterqueue
# Or use Azure Service Bus Explorer (recommended for bulk operations)DLQ Review Cadence
Dead-letter queues should be reviewed weekly in stage and daily in production. Accumulating DLQ messages indicates a processing bug. Each DLQ message has DeadLetterReason and DeadLetterErrorDescription properties that identify the failure cause.
Bicep Configuration
bicep
// service-bus.bicep
resource asbNamespace 'Microsoft.ServiceBus/namespaces@2022-10-01-preview' = {
name: '${resourcePrefix}-asb'
location: location
sku: {
name: environment == 'production' ? 'Premium' : 'Standard'
tier: environment == 'production' ? 'Premium' : 'Standard'
capacity: environment == 'production' ? 1 : 0
}
properties: {
minimumTlsVersion: '1.2'
publicNetworkAccess: environment == 'production' ? 'Disabled' : 'Enabled'
disableLocalAuth: environment == 'production' // MI-only in prod
}
}Related Documentation
- ADR-005: Service Bus — Decision record for ASB over RabbitMQ
- Container Apps — KEDA Service Bus scaling trigger
- Key Vault —
AzureServiceBus--ConnectionStringsecret - Bicep Modules —
service-bus.bicepmodule