🛒 Part 5 — Real-World E-Commerce Microservices with Azure Service Bus & .NET

Over the past 4 parts, we’ve built individual patterns with Azure Service Bus:

  • Part 1 → Queues + Worker Services
  • Part 2 → Topics + Subscriptions (Pub/Sub)
  • Part 3 → DLQ + Retries + Monitoring
  • Part 4 → Sessions, Duplicate Detection, Transactions

👉 In this final part, we’ll combine all these concepts into a real-world e-commerce system.


🏗️ Architecture Overview

Imagine a simple e-commerce platform where customers place orders:

Customer → Order API → Service Bus (Topic)
                                       ↘ Inventory Service (subscription)
                                       ↘ Billing Service (subscription)
                                       ↘ Email Service (subscription)
                                       ↘ Analytics Service (subscription)
  • Order API → accepts orders and publishes events to a Service Bus Topic (orders-topic).
  • Inventory Service → updates stock levels (requires ordering → uses Sessions).
  • Billing Service → charges customer (requires deduplication).
  • Email Service → sends confirmation (fire-and-forget).
  • Analytics Service → logs events for BI dashboards.

🔹 Step 1 — Service Bus Setup

  1. Create namespace
az servicebus namespace create -g myRg -n ecommerce-sb --location eastus
  1. Create topic
az servicebus topic create -g myRg --namespace-name ecommerce-sb -n orders-topic
  1. Create subscriptions
az servicebus subscription create -g myRg --namespace-name ecommerce-sb --topic-name orders-topic -n inventory-sub
az servicebus subscription create -g myRg --namespace-name ecommerce-sb --topic-name orders-topic -n billing-sub
az servicebus subscription create -g myRg --namespace-name ecommerce-sb --topic-name orders-topic -n email-sub
az servicebus subscription create -g myRg --namespace-name ecommerce-sb --topic-name orders-topic -n analytics-sub

🔹 Step 2 — Order API (Publisher)

OrderApi/Controllers/OrdersController.cs

using Azure.Messaging.ServiceBus;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    private readonly ServiceBusSender _sender;

    public OrdersController(ServiceBusClient client, IConfiguration config)
    {
        _sender = client.CreateSender(config["ServiceBus:TopicName"]);
    }

    [HttpPost]
    public async Task<IActionResult> PlaceOrder([FromBody] Order order)
    {
        var json = System.Text.Json.JsonSerializer.Serialize(order);
        var message = new ServiceBusMessage(json)
        {
            ContentType = "application/json",
            Subject = "OrderPlaced",
            MessageId = order.Id.ToString(), // important for deduplication
            SessionId = $"Order-{order.Id}"   // groups related events
        };

        await _sender.SendMessageAsync(message);
        return Accepted(new { order.Id, Status = "Queued" });
    }
}

public record Order(int Id, string Product, int Quantity, decimal Price);

appsettings.json

"ServiceBus": {
  "ConnectionString": "<YOUR_SERVICEBUS_CONNECTION_STRING>",
  "TopicName": "orders-topic"
}

🔹 Step 3 — Inventory Service (Session Consumer)

Ensures FIFO updates for each order.

var processor = client.CreateSessionProcessor("orders-topic", "inventory-sub", new ServiceBusSessionProcessorOptions
{
    MaxConcurrentSessions = 10,
    AutoCompleteMessages = false
});

processor.ProcessMessageAsync += async args =>
{
    Console.WriteLine($"[Inventory][{args.Message.SessionId}] Updating stock for {args.Message.Body}");
    await args.CompleteMessageAsync(args.Message);
};

processor.ProcessErrorAsync += args =>
{
    Console.WriteLine($"Inventory error: {args.Exception}");
    return Task.CompletedTask;
};

await processor.StartProcessingAsync();

🔹 Step 4 — Billing Service (Deduplication + Transactions)

Avoids double-charging.

var processor = client.CreateProcessor("orders-topic", "billing-sub");

processor.ProcessMessageAsync += async args =>
{
    try
    {
        var order = System.Text.Json.JsonSerializer.Deserialize<Order>(args.Message.Body);
        Console.WriteLine($"[Billing] Charging order {order!.Id}...");

        // Idempotency check: MessageId = OrderId
        // Ensure your DB/Redis checks if already processed
        bool alreadyProcessed = await BillingDb.IsProcessedAsync(order.Id);
        if (alreadyProcessed)
        {
            Console.WriteLine($"[Billing] Order {order.Id} already charged.");
            await args.CompleteMessageAsync(args.Message);
            return;
        }

        // Transaction: complete message + record billing
        using var ts = await client.CreateTransactionAsync();
        await args.CompleteMessageAsync(args.Message, ts);

        await BillingDb.MarkProcessedAsync(order.Id, ts);
        await client.CommitTransactionAsync(ts);
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Billing failed: {ex.Message}");
        await args.AbandonMessageAsync(args.Message);
    }
};

🔹 Step 5 — Email Service (Simple Consumer)

Fire-and-forget confirmation.

var processor = client.CreateProcessor("orders-topic", "email-sub");

processor.ProcessMessageAsync += async args =>
{
    Console.WriteLine($"[Email] Sending confirmation for: {args.Message.Body}");
    await args.CompleteMessageAsync(args.Message);
};

🔹 Step 6 — Analytics Service (Monitoring Consumer)

Captures events for BI dashboards.

var processor = client.CreateProcessor("orders-topic", "analytics-sub");

processor.ProcessMessageAsync += async args =>
{
    Console.WriteLine($"[Analytics] Logging event: {args.Message.Body}");
    // Optionally forward to Cosmos DB or Data Lake
    await args.CompleteMessageAsync(args.Message);
};

🔹 Step 7 — Dead Letter Queue (DLQ) Monitor

Each subscription (inventory-sub/$DeadLetterQueue, etc.) has a DLQ.
Example DLQ processor:

var receiver = client.CreateReceiver("orders-topic", "inventory-sub", new ServiceBusReceiverOptions
{
    SubQueue = SubQueue.DeadLetter
});

var messages = await receiver.ReceiveMessagesAsync(10);

foreach (var msg in messages)
{
    Console.WriteLine($"[DLQ][Inventory] Id={msg.MessageId}, Reason={msg.DeadLetterReason}");
    await receiver.CompleteMessageAsync(msg);
}

🔹 Step 8 — Monitoring with App Insights

Enable in each service:

builder.Services.AddApplicationInsightsTelemetry();

Track custom telemetry:

telemetry.TrackEvent("OrderProcessed", new Dictionary<string, string>
{
    { "OrderId", order.Id.ToString() },
    { "Service", "Billing" }
});

KQL Query example:

customEvents
| where name == "OrderProcessed"
| summarize Count = count() by customDimensions.Service

📌 End-to-End Flow

  1. Customer places order → API publishes to orders-topic.
  2. Inventory Service → updates stock (session ensures ordering).
  3. Billing Service → charges (deduplication ensures idempotency).
  4. Email Service → sends confirmation.
  5. Analytics Service → logs for BI dashboards.
  6. DLQ Processor → monitors failures.
  7. App Insights → gives visibility across services.

✅ Best Practices

  • Use Sessions only where strict ordering is required.
  • Always set MessageId for deduplication & idempotency.
  • Use Transactions for workflows spanning multiple operations.
  • Monitor DLQs and set up alerts.
  • Add Application Insights everywhere for observability.
  • Secure services with Managed Identity + RBAC instead of connection strings.

🎯 Conclusion

With Azure Service Bus, we’ve built a real-world e-commerce microservice system that is:

  • Decoupled → services don’t depend on each other
  • Resilient → retries, DLQs, idempotency
  • Scalable → independent consumers scale as needed
  • Observable → full monitoring with App Insights


Comments

Popular posts from this blog

📬 Part 4 — Sessions, Duplicate Detection & Transactions in Azure Service Bus with .NET

📬 Part 3 — Dead Letter Queue, Retries, and Monitoring in Azure Service Bus with .NET