Skip to content

RabbitMQ Interview Prep


1. What Is RabbitMQ

RabbitMQ is a message broker — it sits between services, accepts messages from producers and routes them to consumers. It decouples services so they don't need to know about each other.

  • Written in Erlang — built for concurrency and fault tolerance
  • Protocol: AMQP 0-9-1
  • Model: Push-based — broker pushes messages to consumers
  • Messages can be durable (survive restarts) or transient

2. Core Concepts

Concept What It Is
Producer Publishes messages. Never sends directly to a queue — always via an exchange
Exchange Receives messages and routes them to queues based on type and bindings
Binding A rule linking an exchange to a queue. Consumer owns and declares this
Queue Stores messages until consumed. Think of it as an async endpoint
Consumer Subscribes to a queue, processes messages, sends ACK when done

Flow:

Producer → Exchange → Binding → Queue → Consumer

3. Exchange Types

Type Routing Use When
Direct Exact routing key match Commands, point-to-point
Topic Pattern matching (* = one word, # = zero or more) Events, domain messaging
Fanout Broadcasts to ALL bound queues, ignores routing key Cache invalidation, broadcast
Headers Routes by message headers, not routing key Complex routing without string keys

Default choice in production: Topic exchange per domain — flexible, costs nothing extra, never backs you into a corner.


4. Ownership Rules

  • Publisher owns the exchange — declares it on startup, documents its routing keys
  • Consumer owns the queue and binding — declares its own queue, binds to whatever exchange it cares about
  • Both declare on startup — idempotent, safe to repeat

This means adding a new consumer requires zero changes to the publisher or any other service.


5. Queue = Async Endpoint

A queue is the messaging equivalent of an HTTP endpoint:

HTTP RabbitMQ
POST /invoice/create invoice.order-created.queue
One handler per endpoint One consumer per queue
Request hits you You pull when ready
Fails under load Buffers under load

A single service can have multiple queues — one per concern. Each queue has its own backpressure, failure isolation, and scaling.


6. Production Topology

Standard pattern: Topic exchange per domain, exact bindings for business logic.

order.exchange    (topic)
payment.exchange  (topic)
invoice.exchange  (topic)

Naming convention:

Exchange:    {domain}.exchange
Queue:       {consumer-service}.{event}.queue
Routing key: {domain}.{entity}.{verb}   e.g. order.created

One event → multiple independent queues:

order.exchange
  ── order.created ──▶ invoice.order-created.queue
  ── order.created ──▶ notification.order-created.queue
  ── order.created ──▶ audit.order-created.queue

Publisher has zero knowledge of consumers. New consumer = new queue + new binding, nothing else changes.


7. Events vs Commands

Event Command
Meaning Something happened Tell a specific service to do something
Receivers Many — all subscribers One — specific service
Exchange Topic (fan out) Direct (point-to-point)
Routing key Type name e.g. OrderCreatedEvent Destination name e.g. invoice-service
Publisher knows receivers? No Yes

Why destination as routing key for commands? If you rename the command class, type-name routing breaks silently. Destination name never changes regardless of class name.


8. Delivery Guarantees

At Most Once

  • autoAck: true — broker removes message on delivery regardless of outcome
  • Messages can be lost, never duplicated
  • Use for: metrics, telemetry, live dashboards where occasional loss is fine

At Least Once

  • autoAck: false — manual ACK after processing
  • Messages never lost, duplicates possible
  • Use for: most production systems — orders, payments, emails

Exactly Once (not natively possible)

  • Not achievable at broker level — mathematically proven impossible in distributed systems (Two Generals Problem)
  • Achieved by: at-least-once + idempotent consumer

9. Why Exactly Once Is Impossible

The ACK itself is a message — and messages can be lost:

Consumer processes ✓
Consumer sends ACK
ACK lost in transit
Broker sees no ACK → re-delivers
Consumer processes again → DUPLICATE

Broker cannot distinguish "consumer crashed" from "ACK was lost." Both look identical. So it must re-deliver — duplicates are the price of reliability.

Solution: Idempotent consumer

public async Task HandleAsync(OrderCreatedEvent message)
{
    if (await _db.ProcessedEvents.AnyAsync(e => e.EventId == message.EventId))
        return;  // duplicate — silently discard

    await using var tx = await _db.BeginTransactionAsync();
    await _orders.CreateAsync(new Order { ... });
    await _db.ProcessedEvents.AddAsync(new ProcessedEvent { EventId = message.EventId });
    await tx.CommitAsync();
}

The EventId on every message is your idempotency key. Store it, check it, wrap in a transaction.


10. Reliability Checklist

Layer What To Do
Publisher Enable publisher confirms — know the broker received it
Queue Declare as durable: true — survives restart
Message Set Persistent = true — written to disk not memory
Consumer autoAck: false — manual ACK only after processing
On failure BasicNack with requeue: false — send to DLQ
Handler Idempotent — safe to call multiple times with same message

Both durable queue AND persistent message must be set together — one without the other doesn't fully protect you.


11. Publisher Confirms

_channel.ConfirmSelect();
_channel.BasicPublish(exchange, routingKey, props, body);
_channel.WaitForConfirmsOrDie(timeout: TimeSpan.FromSeconds(5));
// throws if broker didn't confirm — you know to retry

Without confirms: fire and forget — you don't know if broker received it.
With confirms: guaranteed the broker stored it before you move on.


12. Dead Letter Queue (DLQ)

Messages end up in DLQ when:

  • Consumer NACKs with requeue: false
  • Message TTL expires
  • Queue length limit exceeded
_channel.QueueDeclare("my-queue", durable: true, arguments: new Dictionary<string, object>
{
    { "x-dead-letter-exchange", "dlx.exchange" },
    { "x-dead-letter-routing-key", "my-queue.dlq" }
});

In production every queue has a trio:

my-queue             ← main processing
my-queue.retry       ← failed messages, retry after delay
my-queue.dlq         ← given up, needs human review

13. Competing Consumers (Scaling)

Multiple instances of the same service consuming from the same queue:

order-created.queue
  ← worker instance 1
  ← worker instance 2    RabbitMQ round-robins across them
  ← worker instance 3

RabbitMQ dispatches each message to exactly one worker. Scale horizontally by running more instances.

Prefetch count — controls how many unacked messages a consumer receives at once:

_channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: false);

prefetchCount: 1 = fair dispatch — worker only gets next message after finishing current one. Without this, a fast consumer hogs all messages while slow ones sit idle.


14. Connection vs Channel

Connection Channel
What TCP connection to broker Lightweight virtual connection inside TCP
Cost Expensive — one per app Cheap — many per connection
Thread safety Shared safely NOT thread safe — one per thread/consumer
Pattern Singleton New channel per publisher/consumer
// One connection — singleton
var connection = factory.CreateConnection();

// One channel per consumer/publisher — not shared
var channel = connection.CreateModel();

15. .NET Library Landscape

Library Topology License Notes
MassTransit v8 Exchange per message type (fanout) Free (Apache 2.0) De facto standard, supported until end 2026
MassTransit v9 Exchange per message type (fanout) Commercial ~$400/mo Paid from 2025
Rebus Two global exchanges (direct + topic) MIT free Best free alternative
NServiceBus Exchange per endpoint + per event type Commercial Most mature, enterprise
Raw RabbitMQ.Client Whatever you want Free Full control, more boilerplate

16. MassTransit Topology

Default: one fanout exchange per message type, auto-named from type:

OrderCreatedEvent    → exchange: MyApp.Contracts:OrderCreatedEvent (fanout)
PaymentFailedEvent   → exchange: MyApp.Contracts:PaymentFailedEvent (fanout)

Consumer registration auto-creates the queue and binding:

services.AddMassTransit(x => {
    x.AddConsumer<OrderCreatedConsumer>();
    x.UsingRabbitMq((ctx, cfg) => cfg.ConfigureEndpoints(ctx));
});

You think: publish message type → consume message type. Queues and exchanges are infrastructure MassTransit manages.


17. Rebus Topology

Two global exchanges for the entire system:

RebusDirect  (direct)  Send()     commands, point-to-point
RebusTopics  (topic)   Publish()  events, pub-sub

One input queue per service. Multiple handlers inside the service dispatched by Rebus internally:

public class OrderCreatedHandler : IHandleMessages<OrderCreatedEvent>
{
    public async Task Handle(OrderCreatedEvent message) { ... }
}

Must call Subscribe<T>() explicitly on startup — Rebus doesn't auto-wire bindings.


18. Saga Pattern

A Saga manages a business process that spans multiple services and messages over time.

Rule: Single service → use database transaction. Cross service boundary → use Saga.

OrderCreatedEvent  [Saga starts]
   Send(ReserveInventoryCommand)
   InventoryReservedEvent  Send(ChargePaymentCommand)
   PaymentSucceededEvent   Send(ShipOrderCommand)
   OrderShippedEvent       [Saga complete]

  PaymentFailedEvent  Send(ReleaseInventoryCommand)  compensating transaction

Saga persists state between messages in a database. State survives service restarts. Supported natively in MassTransit, NServiceBus, and Rebus.


19. The Abstraction Pattern in .NET

Mirrors the HTTP client abstraction pattern:

HTTP RabbitMQ
IHttpClient interface IMessageBus interface
BaseHttpClient RabbitMqMessageBus
GetOrders() domain wrapper PublishOrderCreated() domain wrapper
IRequestHandler IMessageHandler<T>
Controller BackgroundService consumer
Refit / RestSharp MassTransit

Shared contracts NuGet = the Swagger of messaging. Publisher owns and publishes it. Consumers reference it. Type name as routing key = compile-time safety, no magic strings.


20. The One Interview Question That Separates Everyone

"How does a new service subscribe to an existing event without changing the publishing service?"

Answer:

  1. Publisher already owns and declares the exchange on startup
  2. New service declares its own queue
  3. New service declares its own binding to the existing exchange with the routing key
  4. Publisher has zero knowledge of the new service
  5. Zero changes to publisher, zero changes to any existing consumer

This answer demonstrates understanding of the entire ownership model.


21. RabbitMQ vs Kafka

RabbitMQ Kafka
Model Smart broker, push-based Dumb broker, pull-based
Sweet spot Thousands of msgs/sec Millions of msgs/sec
Message replay No — consumed and gone Yes — replay entire history
Ordering Per queue only Per partition
Use case Task queues, commands, events Event streaming, audit logs, analytics
Retention Until consumed Configurable, can keep forever

RabbitMQ is not outgrown by most businesses — database, API, or business logic becomes the bottleneck first.


22. Key One-Liners For Interviews

  • Exchange routes, queue stores, binding connects them
  • Publisher owns the exchange, consumer owns the queue
  • Queue is an async endpoint — one concern, one handler
  • Topic exchange is the safe default — it does everything Direct does and more
  • autoAck: false is non-negotiable in production
  • Exactly-once = at-least-once + idempotent consumer
  • Saga = a transaction that cannot be completed within a single service
  • RabbitMQ is a protocol — conventions give the topology meaning, not technical constraints
  • The exchange type signals intent to your team, not capability to the broker