Learnixo
Back to blog
AI Systemsintermediate

Azure Service Bus — Reliable Messaging Between Services

Use Azure Service Bus in .NET: publishing and consuming messages, queues vs topics, dead-letter queues, message sessions, Managed Identity authentication, and reliable delivery patterns for clinical events.

Asma Hafeez KhanMay 16, 20265 min read
AzureService BusMessaging.NETMicroservices
Share:𝕏

Service Bus vs In-Process Events

In-process MediatR events (modular monolith):
  → Same process, in-memory — no network
  → No durability: if the process crashes, the event is lost
  → No cross-service delivery: only modules in the same process can subscribe

Azure Service Bus (microservices or cross-process):
  → Messages persist until consumed — survives process restarts
  → Multiple consumers (subscriptions) can receive the same message
  → Dead-letter queue for unprocessable messages
  → Order guarantees with sessions

Use Azure Service Bus when:
  → Two services need to communicate reliably
  → The publisher and consumer are in different deployments
  → Messages must not be lost on publisher failure
  → You need to fan out one event to multiple subscribers

Setup and Registration

C#
// NuGet: Azure.Messaging.ServiceBus

builder.Services.AddSingleton(sp =>
    new ServiceBusClient(
        builder.Configuration["ServiceBus:ConnectionString"],
        new ServiceBusClientOptions
        {
            TransportType = ServiceBusTransportType.AmqpWebSockets
        }));

// Or with Managed Identity (no connection string key):
builder.Services.AddSingleton(sp =>
    new ServiceBusClient(
        fullyQualifiedNamespace: "clinical-servicebus.servicebus.windows.net",
        credential: new DefaultAzureCredential()));

Publishing a Message

C#
public interface IIntegrationEventBus
{
    Task PublishAsync<T>(T @event, CancellationToken ct = default) where T : class;
}

public sealed class ServiceBusIntegrationEventBus : IIntegrationEventBus
{
    private readonly ServiceBusClient _client;

    public ServiceBusIntegrationEventBus(ServiceBusClient client) => _client = client;

    public async Task PublishAsync<T>(T @event, CancellationToken ct = default) where T : class
    {
        var topicName = GetTopicName(typeof(T)); // e.g. "prescription-created"
        await using var sender = _client.CreateSender(topicName);

        var message = new ServiceBusMessage(JsonSerializer.SerializeToUtf8Bytes(@event))
        {
            ContentType    = "application/json",
            Subject        = typeof(T).Name,
            MessageId      = Guid.NewGuid().ToString(),
            CorrelationId  = Activity.Current?.TraceId.ToString()
        };

        await sender.SendMessageAsync(message, ct);
    }

    private static string GetTopicName(Type eventType) =>
        eventType.Name
            .Replace("Event", string.Empty)
            .ToLowerInvariant()
            .Replace("integrationevent", string.Empty)
            + "-events"; // "PrescriptionCreatedIntegrationEvent" → "prescriptioncreated-events"
}

// Example: publish prescription created event after saving to DB
await _eventBus.PublishAsync(new PrescriptionCreatedIntegrationEvent(
    PrescriptionId: prescription.Id.Value,
    PatientId:      prescription.PatientId.Value,
    Medication:     prescription.MedicationName.Value,
    CreatedAt:      DateTime.UtcNow), ct);

Consuming Messages (Background Service)

C#
public sealed class PrescriptionCreatedConsumer : BackgroundService
{
    private readonly ServiceBusClient _client;
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ILogger<PrescriptionCreatedConsumer> _logger;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var processor = _client.CreateProcessor(
            topicName:        "prescriptioncreated-events",
            subscriptionName: "pharmacy-service",
            new ServiceBusProcessorOptions
            {
                MaxConcurrentCalls   = 4,
                AutoCompleteMessages = false // manually complete to control acknowledgement
            });

        processor.ProcessMessageAsync += HandleMessageAsync;
        processor.ProcessErrorAsync   += HandleErrorAsync;

        await processor.StartProcessingAsync(stoppingToken);
        await Task.Delay(Timeout.Infinite, stoppingToken);
        await processor.StopProcessingAsync();
    }

    private async Task HandleMessageAsync(ProcessMessageEventArgs args)
    {
        using var scope   = _scopeFactory.CreateScope();
        var handler       = scope.ServiceProvider
            .GetRequiredService<IIntegrationEventHandler<PrescriptionCreatedIntegrationEvent>>();

        var @event = JsonSerializer.Deserialize<PrescriptionCreatedIntegrationEvent>(
            args.Message.Body)!;

        await handler.HandleAsync(@event, args.CancellationToken);

        // Complete the message — removes it from the queue
        await args.CompleteMessageAsync(args.Message);
    }

    private Task HandleErrorAsync(ProcessErrorEventArgs args)
    {
        _logger.LogError(args.Exception,
            "Service Bus error on {Source}", args.ErrorSource);
        return Task.CompletedTask;
        // Message is NOT completed — Service Bus retries based on MaxDeliveryCount
    }
}

Dead-Letter Queue

C#
// Messages that fail MaxDeliveryCount times are moved to the dead-letter queue (DLQ)
// Default MaxDeliveryCount: 10

// Process dead-lettered messages for investigation and manual reprocessing:
var dlqProcessor = _client.CreateProcessor(
    topicName:        "prescriptioncreated-events",
    subscriptionName: "$DeadLetterQueue/pharmacy-service");

// Monitor DLQ message count — if it grows, something is systematically failing
// Alert: "DLQ count > 0 for more than 5 minutes" in Azure Monitor

// Inspect DLQ messages:
await using var receiver = _client.CreateReceiver(
    "prescriptioncreated-events",
    "$DeadLetterQueue/pharmacy-service");

var dlqMessages = await receiver.ReceiveMessagesAsync(maxMessages: 20);
foreach (var msg in dlqMessages)
{
    _logger.LogError(
        "DLQ message {MessageId}: DeadLetterReason={Reason}, Body={Body}",
        msg.MessageId,
        msg.DeadLetterReason,
        msg.Body.ToString());
}

Message Sessions for Ordered Processing

C#
// Use sessions when messages for the same patient must be processed in order
// All messages with the same SessionId are processed by the same consumer instance

var message = new ServiceBusMessage(payload)
{
    SessionId = patientId.ToString() // all messages for this patient go to same session
};

// Session processor guarantees order within a session
var sessionProcessor = _client.CreateSessionProcessor(
    topicName:        "patient-events",
    subscriptionName: "audit-service",
    new ServiceBusSessionProcessorOptions
    {
        MaxConcurrentSessions = 10 // process up to 10 patients' events concurrently
    });

Production issue I've seen: A microservices system used Azure Service Bus for inter-service events. A consumer in the Pharmacy service had a bug in its message handler — a null reference exception when the medication name was null. The handler threw on every message, Service Bus retried 10 times (MaxDeliveryCount = 10), and after 10 failures the message moved to the dead-letter queue. The bug went unnoticed for 3 days. By then, 4,700 prescription events were in the DLQ — none had triggered the pharmacy dispense workflow. Nobody was monitoring the DLQ message count. Adding an Azure Monitor alert for "DLQ count greater than 0 for more than 5 minutes" would have surfaced this within minutes of the first failure.


Key Takeaway

Azure Service Bus provides durable, at-least-once message delivery between services. Use topics and subscriptions for fan-out (multiple subscribers per event). Always use AutoCompleteMessages = false and complete messages manually after successful processing. Configure dead-letter queue alerts — unmonitored DLQ growth indicates a systematic processing failure. Use sessions (SessionId per patient or aggregate) when processing order matters. Authenticate with Managed Identity — no connection string secrets in configuration.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.