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:
- Order Creation → OrderService publishes the order
- Inventory Check → InventoryService verifies stock availability
- Invoice Generation → InvoiceService creates invoices and determines shipping
- Shipping → ShippingService handles domestic/international delivery
- 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
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
- .NET 9.0 SDK
- RabbitMQ Server running locally
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:
- DeadLetterService
- InventoryService
- InvoiceService
- ShippingService
- NotificationService
- EmailNotificationService
- SmsNotificationService
- 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
Post a Comment