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:
- Publisher already owns and declares the exchange on startup
- New service declares its own queue
- New service declares its own binding to the existing exchange with the routing key
- Publisher has zero knowledge of the new service
- 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