Backend Development 12 min read

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.

Architects Research Society
Architects Research Society
Architects Research Society
Best Practices for Communicating Between Microservices Using RabbitMQ and NServiceBus

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.

BackendmicroservicesRabbitMQAsynchronous CommunicationMessage BrokerNServiceBus
Architects Research Society
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.