Building a Microservices Order Processing System with RabbitMQ and .NET 9.0


Building a Microservices Order Processing System with RabbitMQ and .NET 9.0

Introduction

In today's fast-paced e-commerce world, building scalable and resilient systems is crucial. This comprehensive tutorial will guide you through creating a complete microservices-based order processing system using .NET 9.0 and RabbitMQ. We'll explore various messaging patterns, error handling, and best practices for building event-driven architectures.

This project serves as an excellent learning resource for developers looking to understand:

  • Microservices communication patterns
  • RabbitMQ exchange types and routing
  • Asynchronous programming in .NET
  • Error handling and dead letter queues
  • Scalable system design

Project Overview

Our order processing system simulates a complete e-commerce fulfillment workflow. When a customer places an order, it flows through multiple services:

  1. Order Creation → OrderService publishes the order
  2. Inventory Check → InventoryService verifies stock availability
  3. Invoice Generation → InvoiceService creates invoices and determines shipping
  4. Shipping → ShippingService handles domestic/international delivery
  5. Notifications → Multiple notification services alert stakeholders

The system demonstrates real-world challenges like failure simulation, message routing, and error recovery through dead letter queues.

System Architecture

The architecture showcases different RabbitMQ exchange types working together:

  • Direct Exchange: Routes order messages to inventory processing
  • Topic Exchange: Routes shipping tasks by type (domestic/international)
  • Fanout Exchange: Broadcasts notifications to all subscribers
  • Dead Letter Exchange: Handles failed messages for analysis
graph TD A[OrderService] --> B[order.exchange] B --> C[order.queue] C --> D[InventoryService] D --> E[invoice.queue] E --> F[InvoiceService] F --> G[shipping.topic.exchange] G --> H[shipping.domestic.queue] G --> I[shipping.international.queue] H --> J[ShippingService] I --> J J --> K[notification.fanout.exchange] K --> L[notification.queue] K --> M[notification.email.queue] K --> N[notification.sms.queue] L --> O[NotificationService] M --> P[EmailNotificationService] N --> Q[SmsNotificationService] D --> R[Dead Letter Queue] R --> S[DeadLetterService]

Message Flow

The complete message flow follows this pattern:

OrderService → order.exchange → order.queue → InventoryService → invoice.queue → InvoiceService → shipping.topic.exchange → shipping queues → ShippingService → notification.fanout.exchange → notification queues → Notification Services

Core Services Breakdown

OrderService - The Message Publisher

The OrderService acts as the entry point, publishing new orders to initiate the processing pipeline.

using RabbitMQ.Client;
using System.Text;
using System.Text.Json;

var factory = new ConnectionFactory() { };
using var connection = await factory.CreateConnectionAsync();
using var channel = await connection.CreateChannelAsync();

await channel.ExchangeDeclareAsync(exchange: "order.exchange", type: ExchangeType.Direct, durable: true);
await channel.QueueBindAsync(queue: "order.queue", exchange: "order.exchange", routingKey: "order.created");

while (true)
{
    var order = new { OrderId = Guid.NewGuid(), ProductId = "P123", Quantity = 2 };
    var msgBody = JsonSerializer.Serialize(order);
    var body = Encoding.UTF8.GetBytes(msgBody);

    Console.WriteLine($"[OrderService] Press ENTER Send Order: {order.OrderId}");
    Console.ReadKey();

    await channel.BasicPublishAsync(exchange: "order.exchange", routingKey: "order.created", body);
    Console.WriteLine($"[OrderService] Sent Order: {order.OrderId}");
}

InventoryService - Stock Verification with Failure Simulation

This service demonstrates error handling by randomly simulating inventory failures.

var consumer = new AsyncEventingBasicConsumer(channel);

consumer.ReceivedAsync += async (sender, e) =>
{
    var body = e.Body.ToArray();
    var msg = Encoding.UTF8.GetString(body);
    var order = JsonDocument.Parse(msg).RootElement;

    string OrderId = order.GetProperty("OrderId").GetString();
    Console.WriteLine($"[InventoryService] Received Order:{OrderId}");

    var random = new Random();
    bool fail = random.Next(0, 2) == 0;

    if (fail)
    {
        Console.WriteLine($"[InventoryService] Simulating failure. Rejecting Order: {OrderId}");
        await channel.BasicRejectAsync(e.DeliveryTag, requeue: false);
        return;
    }

    await Task.Delay(1000);
    Console.WriteLine($"[InventoryService] Stock available. Order processed.");

    await channel.BasicAckAsync(deliveryTag: e.DeliveryTag, multiple: false);

    // Forward to InvoiceService
    var invoiceMessage = JsonSerializer.Serialize(new { OrderId, ProductId, Quantity });
    var notificationBody = Encoding.UTF8.GetBytes(invoiceMessage);
    await channel.BasicPublishAsync("", "invoice.queue", notificationBody);
}; 

InvoiceService - Invoice Generation and Shipping Routing

Generates invoices and determines shipping types using topic exchange routing.

consumer.ReceivedAsync += async (sender, e) =>
{
    var body = e.Body.ToArray();
    var msg = Encoding.UTF8.GetString(body);
    var order = JsonDocument.Parse(msg).RootElement;

    string OrderId = order.GetProperty("OrderId").GetString();
    Console.WriteLine($"[InvoiceService] Received Order: {OrderId}");

    await Task.Delay(1000); // Simulate invoice generation

    var shippingType = (DateTime.Now.Second % 2 == 0) ? "domestic" : "international";
    var routingKey = $"shipping.{shippingType}";

    var invoiceMessage = JsonSerializer.Serialize(new
    {
        OrderId,
        ShippingType = shippingType
    });

    var invoice = Encoding.UTF8.GetBytes(invoiceMessage);
    await channel.BasicPublishAsync("shipping.topic.exchange", routingKey, invoice);
    Console.WriteLine($"[InvoiceService] Invoice sent with routing key: {routingKey}");

    await channel.BasicAckAsync(e.DeliveryTag, false);
}; 

ShippingService - Multi-Queue Consumer

Handles both domestic and international shipping using separate consumers.

var domesticConsumer = new AsyncEventingBasicConsumer(channel);
var internationalConsumer = new AsyncEventingBasicConsumer(channel);

domesticConsumer.ReceivedAsync += async (sender, e) =>
{
    var body = e.Body.ToArray();
    var msg = Encoding.UTF8.GetString(body);
    var order = JsonDocument.Parse(msg).RootElement;

    string OrderId = order.GetProperty("OrderId").GetString();
    string ShippingType = order.GetProperty("ShippingType").GetString();

    Console.WriteLine($"[ShippingService - Domestic] Shipping Order: {OrderId}");
    await Task.Delay(1000);
    Console.WriteLine($"[ShippingService - Domestic] Shipped successfully.");

    await channel.BasicAckAsync(deliveryTag: e.DeliveryTag, multiple: false);

    // Publish notification
    var notificationMessage = JsonSerializer.Serialize(new
    {
        OrderId,
        ShippingType,
        Status = "Shipped"
    });

    var notificationBody = Encoding.UTF8.GetBytes(notificationMessage);
    await channel.BasicPublishAsync("notification.fanout.exchange", "", notificationBody);
}; 

Notification Services - Fanout Exchange Consumers

Multiple services consume from the same fanout exchange for different notification channels.

// EmailNotificationService
consumer.ReceivedAsync += async (sender, e) =>
{
    var body = e.Body.ToArray();
    var msg = Encoding.UTF8.GetString(body);
    var order = JsonDocument.Parse(msg).RootElement;

    string OrderId = order.GetProperty("OrderId").GetString();

    Console.WriteLine($"[EmailNotificationService] Sending EMAIL for Order: {OrderId}");
    await Task.Delay(500);
    Console.WriteLine($"[EmailNotificationService] EMAIL sent successfully.");

    await channel.BasicAckAsync(e.DeliveryTag, false);
}; 

DeadLetterService - Error Message Handler

Processes messages that couldn't be handled by other services.

consumer.ReceivedAsync += async (sender, e) =>
{
    var body = e.Body.ToArray();
    var msg = Encoding.UTF8.GetString(body);
    var order = JsonDocument.Parse(msg).RootElement;

    string OrderId = order.GetProperty("OrderId").GetString();

    Console.WriteLine($"[DeadLetterService] Received dead-lettered message for Order: {OrderId}");
    await Task.Delay(500);
    await channel.BasicAckAsync(e.DeliveryTag, false);
}; 

RabbitMQ Configuration Details

Exchange Setup

// Direct Exchange for orders
await channel.ExchangeDeclareAsync("order.exchange", ExchangeType.Direct, durable: true);

// Topic Exchange for shipping
await channel.ExchangeDeclareAsync("shipping.topic.exchange", ExchangeType.Topic, true);

// Fanout Exchange for notifications
await channel.ExchangeDeclareAsync("notification.fanout.exchange", ExchangeType.Fanout, true);

// Dead Letter Exchange
await channel.ExchangeDeclareAsync("dlx.exchange", ExchangeType.Fanout, true); 

Queue Configuration with DLX

var dlxOptions = new Dictionary
{
    { "x-dead-letter-exchange", "dlx.exchange" },
    { "x-message-ttl", 10000 } // 10 seconds TTL
};

await channel.QueueDeclareAsync("order.queue", durable: true, exclusive: false, autoDelete: false, arguments: dlxOptions);
await channel.QueueDeclareAsync("dlq.queue", durable: true, exclusive: false, autoDelete: false); 

Setup and Installation

Prerequisites

Alternative: Docker Setup

docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:management 

Project Setup

git clone https://github.com/your-username/rabbitmq-practise.git
cd rabbitmq-practise
dotnet restore 

Running the System

Start services in this order:

  1. DeadLetterService
  2. InventoryService
  3. InvoiceService
  4. ShippingService
  5. NotificationService
  6. EmailNotificationService
  7. SmsNotificationService
  8. OrderService

Run each service:

dotnet run --project DeadLetterService
dotnet run --project InventoryService
# ... continue for each service 

In the OrderService terminal, press ENTER to create and send orders. Watch the message flow across all service consoles.

Examples and Output

Successful Order Flow

[OrderService] Sent Order: 123e4567-e89b-12d3-a456-426614174000
[InventoryService] Received Order: 123e4567-e89b-12d3-a456-426614174000
[InventoryService] Stock available. Order processed.
[InvoiceService] Received Order: 123e4567-e89b-12d3-a456-426614174000
[ShippingService - Domestic] Shipping Order: 123e4567-e89b-12d3-a456-426614174000
[NotificationService] Sending notification for Order: 123e4567-e89b-12d3-a456-426614174000
[EmailNotificationService] Sending EMAIL for Order: 123e4567-e89b-12d3-a456-426614174000
[SmsNotificationService] Sending SMS for Order: 123e4567-e89b-12d3-a456-426614174000 

Failure Scenario

[InventoryService] Simulating failure. Rejecting Order: 123e4567-e89b-12d3-a456-426614174000
[DeadLetterService] Received dead-lettered message for Order: 123e4567-e89b-12d3-a456-426614174000 

Conclusion

This RabbitMQ order processing system demonstrates the power of event-driven microservices architecture. By leveraging different exchange types and proper error handling, we've created a resilient and scalable system that can handle real-world e-commerce scenarios.

Key takeaways:

  • RabbitMQ's flexibility in routing messages
  • Importance of dead letter queues for error handling
  • Benefits of asynchronous communication
  • Scalability through independent service deployment

Have questions or suggestions? Leave a comment below!

Comments

Popular posts from this blog

Complete Beginner's Guide to C# Types: Class vs Record vs Struct + Pattern Matching

What is the Actor Model? A Complete Guide for C# Developers