.NET Core  

Building distributed system using RabbitMQ and .NET

Modern software systems are no longer built as single large applications. As systems scale, teams grow, and business domains expand, traditional monolithic architectures begin to struggle with performance, scalability, and deployment complexity.

In this article, we’ll explore how to build a distributed system using:

  • Microservices Architecture

  • RabbitMQ Message Broker

  • .NET (ASP.NET Core)

  • Event-Driven Communication

Introduction to Microservices

Microservices architecture is a software design approach where an application is built as a collection of small, independent services, each responsible for a specific business capability. These services run as separate processes and communicate with one another through well-defined, lightweight APIs.

Each microservice is focused on performing a single function, which makes the system modular and easier to understand, develop, and maintain. Because the services are loosely coupled, they can be developed, deployed, updated, and scaled independently without affecting the entire application. This flexibility allows organizations to respond quickly to changes, improve fault isolation, and optimize performance based on demand for individual components.

Message Broker (RabbitMQ)

RabbitMQ is an open-source message broker written in Erlang and implemented according to the Advanced Message Queuing Protocol (AMQP), allowing for the exchange of messages in a distributed environment through microservices. RabbitMQ operates by having producers sending messages to exchanges, which will then distribute them via queues depending on routing rules, from which the messages are read by consumers.

Some of the key elements of RabbitMQ include producers, consumers, queues, exchanges, bindings, and routing keys. RabbitMQ is preferred for its reliability through message persistence and acknowledgement, scalability for handling large volumes of messages and clustering capabilities, flexibility regarding message patterns such as point to point, publish and subscribe, and request/reply, and ease of use through management tools and compatibility with many programming languages.

Event Driven Communication in Microservices

In a microservices architecture, event-driven communication is an asynchronous design pattern where services interact by publishing and subscribing to events rather than making direct, synchronous calls (like REST or gRPC). When a service undergoes a state change (e.g., “Order Placed in an e-commerce”), it emits an event, and any other service (e.g., Billing and Inventory) interested in that change reacts to it independently.

Key Benefits of Microservices

  • Loose Coupling : Services don’t need to know about each other’s existence or internal implementation; they only interact with the event broker.

  • High Scalability : Producers and consumers can be scaled independently based on their specific workload.

  • Improved Resilience : If a consumer service is temporarily down, the event broker holds the message until the service is back online, preventing system-wide failures.

  • Asynchronous Processing : Systems remain responsive because they don’t block while waiting for a response from another service.

Step by Step Implementation: Shopping App

We will implement a distributed microservices system for the shopping application. The system will have a Point of Sale (POS) system that publishes a sales event to RabbitMQ. The Billing and Inventory microservices will independently consume and process that event.

Step 1 Install Docker Desktop

RabbitMQ runs best inside containers. To download use the following link: Docker

After installation start the docker desktop and ensure Docker engine is running.

Step 2 Pull the Official RabbitMQ Image

Open the terminal at docker desktop and pull the rabbitmq image.

docker pull rabbitmq:management

Step 3 Run RabbitMQ Container

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

Port 5672 will be used by AMQP Messaging and Port 15672 will be used by Web Management UI.

Step 4 Access RabbitMQ Dashboard

Open the management UI in browser: http://localhost:15672

For login, use a credentials
Username: guest
Password: guest

We can see the RabbitMQ dashboard showing Exchanges, Queues, Connections, Message statistics.

Step 5 Setting up .NET Application (ShoppingApp)

We will build an application composed of multiple microservices (Console Apps for the simplicity).

  • ShoppingApp.POS — Publishes sales events (Console App)

  • ShoppingApp.Billing — Generates invoices (Console App)

  • ShoppingApp.Inventory — Updates stock (Console App)

  • ShoppingApp.Contracts — Shared message contracts (C# Class Library)

Step 6 Create Solution Structure

Create a Console Application: ShoppingApp

Add the following projects:

  • ShoppingApp.POS

  • ShoppingApp.Billing

  • ShoppingApp.Inventory

  • ShoppingApp.Contracts

The Contracts project contains shared class used by all services.

Step 7 Add a common class SaleDetails inside Contracts

SaleDetails.cs

public record SaleDetails(
    int SaleId,
    string StoreId,
    List<SaleItem> Items,
    DateTime Timestamp,
    decimal TotalAmount
    );
public record SaleItem(string Sku, int Quantity);

This record represents the event message transmitted between the microservices.

Step 8 Add RabbitMQ.Client NuGet package

Add RabbitMQ.Client package to all the projects.

Step 9 Configure Startup Projects

Set the following projects as multiple startup projects:

  • ShoppingApp.POS

  • ShoppingApp.Billing

  • ShoppingApp.Inventory

Step 10 Implement POS Publisher

Inside the program.cs file of the ShoppingApp.POS paste the below code for publishing the sales event to the message broker.

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

int i = 0;
while (true)
{
    Console.Write("Press a to add new item and q to cancel/stop: ");
    ConsoleKeyInfo key = Console.ReadKey();
    Console.WriteLine();

    if (key.Key == ConsoleKey.A)
    {
        await CreateEvent(i++);
    }
    else if (key.Key == ConsoleKey.Q)
    {
        break;
    }
}

static async Task CreateEvent(int eventId)
{
    ConnectionFactory factory = new ConnectionFactory { HostName = "localhost", Port = 5672 };
    using IConnection connection = await factory.CreateConnectionAsync();
    using IChannel channel = await connection.CreateChannelAsync();

    await channel.ExchangeDeclareAsync("sales_exchange", ExchangeType.Topic, durable: true);

    SaleDetails sale = new SaleDetails(
        SaleId: 10000 + eventId,
        StoreId: "NEPAL-01",
        Items: [new($"MILK00{eventId}", 5), new($"BREAD00{eventId}", 5)],
        Timestamp: DateTime.UtcNow,
        TotalAmount: 10.0m
    );

    string message = JsonSerializer.Serialize(sale);
    byte[] body = Encoding.UTF8.GetBytes(message);

    BasicProperties properties = new BasicProperties();
    await channel.BasicPublishAsync(
        exchange: "sales_exchange",
        routingKey: "sale.created",
        mandatory: true,
        basicProperties: properties,
        body: body
    );

    Console.WriteLine($"Sent SaleEvent #{sale.SaleId}.");
    Console.WriteLine("Press [Enter] to exit.");
    Console.ReadLine();
}

Here we have created a sale event. And that event is published to sales_exchange and routing is used as sale.created.

Step 11 Implement Billing Consumer

Billing listens for sale events and generates invoices.

Program.cs (ShoppingApp.Billing)

var factory = new ConnectionFactory { HostName = "localhost", Port = 5672 };
using var connection = await factory.CreateConnectionAsync();
using var channel = await connection.CreateChannelAsync();

await channel.ExchangeDeclareAsync("sales_exchange", ExchangeType.Topic, durable: true);
await channel.QueueDeclareAsync("billing_queue", durable: true, exclusive: false, autoDelete: false);
await channel.QueueBindAsync("billing_queue", "sales_exchange", "sale.created");

var consumer = new AsyncEventingBasicConsumer(channel);
consumer.ReceivedAsync += async (s, e) =>
{
    var body = e.Body.ToArray();
    var json = Encoding.UTF8.GetString(body);
    var sale = JsonSerializer.Deserialize<SaleDetails>(json);

    Console.WriteLine($"Generating invoice for Sale #{sale?.SaleId} - Total: Rs.{sale?.TotalAmount}");
    await channel.BasicAckAsync(e.DeliveryTag, false);
};

await channel.BasicConsumeAsync("billing_queue", autoAck: false, consumer);
Console.WriteLine("Billing Service running. Press [Enter] to exit.");
Console.ReadLine();

Step 12 Implement Inventory Consumer

Inventory independently processes the same event.

Program.cs (ShoppingApp.Inventory)

var factory = new ConnectionFactory { HostName = "localhost", Port = 5672 };
using var connection = await factory.CreateConnectionAsync();
using var channel = await connection.CreateChannelAsync();

await channel.ExchangeDeclareAsync("sales_exchange", ExchangeType.Topic, durable: true);
await channel.QueueDeclareAsync("inventory_queue", durable: true, exclusive: false, autoDelete: false);
await channel.QueueBindAsync("inventory_queue", "sales_exchange", "sale.created");

var consumer = new AsyncEventingBasicConsumer(channel);
consumer.ReceivedAsync += async (s, e) =>
{
    var body = e.Body.ToArray();
    var json = Encoding.UTF8.GetString(body);
    var sale = JsonSerializer.Deserialize<SaleDetails>(json);

    Console.WriteLine($"Inventory update for Sale #{sale?.SaleId}, Items: {sale?.Items.Count}");
    await channel.BasicAckAsync(e.DeliveryTag, false);
};

Step 13 Run the Project

Run all three projects simultaneously.

As we can see the message published and receieved on the console output. The workflow will be like this:

Press a in POS application -> POS publishes event -> Billing receives message -> Inventory receives message.

For one publish event from POS, there could be a multiple independent reaction from Billing and Inventory services.

Step 14 Verify in RabbitMQ Dashboard

Open: http://localhost:15672

We can observe that sales_exchange is created, billing_queue and inventory_queue also created. Inside each queue we can see message rates, consumers, acknowledgements, and delivery statistics.

Here, RabbitMQ acts as the backbone of communication which ensures reliable message delivery while allow other services to react asynchronously to business activities (message). With this approach we can scale the system with demand, and remain operable under failure, and aligns software design with real-world business workflows. More importantly, it shifts the engineering mindset from building applications around APIs to designing systems around events and domain behavior.

The source code used on this article is available on Github.