Best Practices for Communicating Between Microservices Using RabbitMQ and NServiceBus
The article explains why synchronous protocols should be avoided in microservice communication, describes how to use RabbitMQ as an asynchronous message broker with various exchange types and routing patterns, provides step‑by‑step C# examples for sender and receiver services, and shows how NServiceBus can further decouple and manage long‑running requests.
Communication Types
Two main communication protocols are discussed: synchronous HTTP, where the client waits for a response, and asynchronous protocols such as AMQP (e.g., RabbitMQ or Kafka) that allow fire‑and‑forget messaging.
Why Avoid Synchronous Protocols
Endpoints proliferate and become tangled when many microservices need to exchange data, especially when passing authentication tokens.
Calls block execution while waiting for potentially slow responses.
Retry logic can create bottlenecks if a request fails.
If a downstream service is down, requests are delayed or lost, causing order‑processing failures in e‑commerce scenarios.
Receiving services may be unable to handle a burst of requests, requiring a buffer.
To address these challenges, an intermediate message‑broker service (e.g., RabbitMQ or Azure Service Bus) can be introduced.
How to Use RabbitMQ for Microservice Communication
RabbitMQ acts as a broker that receives messages from a publisher via an Exchange and routes them to one or more queues. Messages stay in the queue until a consumer retrieves and processes them.
Exchange Types
Direct exchange – routes based on a routing key (default).
Fan‑out exchange – broadcasts to all bound queues.
Headers exchange – routes according to message header values.
Topic exchange – uses pattern matching with wildcards for flexible routing.
Example routing keys:
order.logs.customer
order.logs.international
order.logs.customer.electronics
order.logs.international.electronics
The pattern order.*.*.electronics matches keys where the first word is order and the fourth word is electronics . The pattern order.logs.customer.# matches any key that starts with order.logs.customer .
Implementing RabbitMQ
Installation
Install RabbitMQ on Windows (e.g., via the official installer). After installation, the management UI is available at http://localhost:15672/ with the default credentials guest/guest .
Creating Sender and Receiver Applications
Build two console applications: Sender to publish messages and Receiver to consume them. Add the RabbitMQ.Client NuGet package to both projects.
using System;
using RabbitMQ.Client;
using System.Text;
class Send
{
public static void Main()
{
var factory = new ConnectionFactory() { HostName = "localhost" };
using(var connection = factory.CreateConnection())
using(var channel = connection.CreateModel())
{
channel.QueueDeclare(queue: "hello", durable: false, exclusive: false, autoDelete: false, arguments: null);
string message = "Hello World!";
var body = Encoding.UTF8.GetBytes(message);
channel.BasicPublish(exchange: "", routingKey: "hello", basicProperties: null, body: body);
Console.WriteLine(" [x] Sent {0}", message);
}
Console.WriteLine(" Press [enter] to exit.");
Console.ReadLine();
}
}The code above creates a connection, declares a queue named hello , and publishes a simple message.
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System;
using System.Text;
class Receive
{
public static void Main()
{
var factory = new ConnectionFactory() { HostName = "localhost" };
using(var connection = factory.CreateConnection())
using(var channel = connection.CreateModel())
{
channel.QueueDeclare(queue: "hello", durable: false, exclusive: false, autoDelete: false, arguments: null);
Console.WriteLine(" [*] Waiting for messages.");
var consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
Console.WriteLine(" [x] Received {0}", message);
};
channel.BasicConsume(queue: "hello", autoAck: true, consumer: consumer);
Console.WriteLine(" Press [enter] to exit.");
Console.ReadLine();
}
}
}This receiver connects to the same queue, registers a handler, and prints incoming messages.
Running both programs shows the queue in the RabbitMQ UI and a spike indicating pending messages. As the system grows, you may need multiple instances for load‑balancing and to handle acknowledgments, retries, and malformed messages.
Using NServiceBus for Further Decoupling
NServiceBus can wrap RabbitMQ, providing higher‑level abstractions such as commands, events, retries, auditing, and error queues.
class Program
{
static async Task Main(string[] args)
{
await CreateHostBuilder(args).RunConsoleAsync();
}
public static IHostBuilder CreateHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args)
.UseNServiceBus(context =>
{
var endpointConfiguration = new EndpointConfiguration("Sales");
endpointConfiguration.UseTransport<LearningTransport>();
endpointConfiguration.SendFailedMessagesTo("error");
endpointConfiguration.AuditProcessedMessagesTo("audit");
return endpointConfiguration;
});
}
}Messages are sent via an IMessageSession :
public class HomeController : Controller
{
private readonly IMessageSession _messageSession;
private readonly ILogger<HomeController> _log;
public HomeController(IMessageSession messageSession, ILogger<HomeController> log)
{
_messageSession = messageSession;
_log = log;
}
[HttpPost]
public async Task
PlaceOrder()
{
var orderId = Guid.NewGuid().ToString().Substring(0, 8);
var command = new PlaceOrder { OrderId = orderId };
await _messageSession.Send(command).ConfigureAwait(false);
_log.LogInformation($"Sending PlaceOrder, OrderId = {orderId}");
// return view etc.
}
}And a handler processes the command:
public class PlaceOrderHandler : IHandleMessages<PlaceOrder>
{
static readonly ILog log = LogManager.GetLogger<PlaceOrderHandler>();
public Task Handle(PlaceOrder message, IMessageHandlerContext context)
{
log.Info($"Received PlaceOrder, OrderId = {message.OrderId}");
return Task.CompletedTask;
}
}This demonstrates a basic NServiceBus + RabbitMQ integration.
Conclusion
Avoid synchronous protocols for inter‑service communication. Use RabbitMQ to buffer messages between services, and consider NServiceBus to further decouple application code from the broker while handling long‑running workflows and error handling.
Architects Research Society
A daily treasure trove for architects, expanding your view and depth. We share enterprise, business, application, data, technology, and security architecture, discuss frameworks, planning, governance, standards, and implementation, and explore emerging styles such as microservices, event‑driven, micro‑frontend, big data, data warehousing, IoT, and AI architecture.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.