![]()
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:
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.