Skip to content

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

EnvironmentNamespaceSKUResource Group
devmic-erp-be-dev-asbStandardmic-erp-be-dev-data-rg
stagemic-erp-be-stage-asbStandardmic-erp-be-stage-data-rg
preprodmic-erp-be-preprod-asbStandardmic-erp-be-preprod-data-rg
uatmic-erp-be-uat-asbStandardmic-erp-be-uat-data-rg
productionmic-erp-be-prod-asbPremiummic-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

TopicPublished ByDescription
invoicesAccountingInvoice created, approved, reversed
vouchersAccountingJournal voucher posted
hr-eventsHREmployee onboarding, leave requests, payroll
workflow-eventsWorkflowStep completed, approval required, escalation
notificationsAll servicesNotification dispatch requests
tenant-eventsBusinessOwnerTenant created, suspended, deleted
import-eventsImport serviceImport 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
  }
}

Internal Documentation — Microtec Platform Team