Interview Handbook by Sayed Elmahdy العربية

Topic 17 · Distributed systems

Messaging & distributed systems

Message brokers, RabbitMQ, Kafka, outbox, idempotency, sagas, resilience.

15 questionsJunior · 3Mid · 8Senior · 410 quiz

Concepts

What you will learn: If an order is saved while the email service is down, keep checkout working and deliver the work later—without losing messages or repeating their effects during retries. We build from producer, , and queue to confirmations, consumers, and recovery after failure.

Before you start: A producer sends a message, a queue holds pending work, and a consumer handles it. Keep database saving separate from sending over the network: they are two operations, and the application can fail between them. Basic and database knowledge is enough; the remaining terms are introduced here.

Select a term with a dotted underline for its meaning, then continue reading from the same place.

#Synchronous calls, asynchronous messaging and brokers

If checkout waits for email and analytics, a secondary outage can prevent a valid purchase. Synchronous fits a decision needed immediately; messaging fits work that can follow the committed order.

A phone call needs both parties available together. A post office accepts a letter and delivers it later according to its address and delivery rules.

A accepts, routes and stores messages according to its configuration. Publishing is not automatically durable, and acceptance does not mean processing finished. A command requests an action from an owner; an event announces a fact that multiple subscribers may care about.

Checkout -> database: order + outbox row in one transaction // (1)
Dispatcher -> broker -> email queue -> email worker          // (2)
                     -> analytics queue -> analytics worker
🧾 Code walkthrough
  1. (1) The HTTP request returns after the local commit, rather than after email delivery.
  2. (2) Each subscriber needs its own queue for a copy; workers sharing one queue compete for messages.

The costs include , duplicates and operational complexity. Expose meaningful pending states and monitor message age. See topic 12 for and consistency; messaging does not automatically guarantee availability.

#RabbitMQ components and exchange types

A producer publishes to an exchange with a routing key. Bindings connect exchanges to queues using routing rules, and consumers read from queues. Connections are normally long-lived, with lighter channels inside them; virtual hosts provide logical and separate permissions.

ExchangeRouting rule and example
directExact match: payment.failed reaches every queue bound with that key.
topicorder.*.eg matches order.paid.eg. Star matches one word; order.# matches zero or more dot-separated words.
fanoutCopies to every bound queue, ignoring the routing key; useful for email and loyalty subscribers.
headersMatches attributes such as format=pdf and region=eg; x-match=all requires all conditions, while any requires one.
default: ""A predeclared direct exchange; queues are automatically bound by their names, so the routing key is the queue name.

The exchange does not store the backlog. A is also an exchange and needs a bound queue to retain failures. These are the core AMQP 0-9-1 exchange forms; RabbitMQ also has extensions and additional types. See the exchange reference.

exchange=orders (topic), routingKey=order.paid.eg
  binding=order.*.eg -> regional-orders
  binding=order.#    -> all-orders
  binding=order.*    -> no match (only two words)
exchange="", routingKey=email -> queue named email

#RabbitMQ.Client 7: publishing and confirms

The application sending a message is the publisher; RabbitMQ is the that receives it and routes it to a queue. A successful socket write means data left your application, not that the broker accepted or retained it. A publisher confirm is the broker’s response about the publish. It differs from a consumer ack, the consumer’s signal after it finishes handling a delivery. This console example requires RabbitMQ.Client 7.x and a local broker; production needs , restricted permissions and external configuration.

using RabbitMQ.Client;
using System.Text;

var factory = new ConnectionFactory { HostName = "localhost" };
await using var connection = await factory.CreateConnectionAsync();
var options = new CreateChannelOptions(
    publisherConfirmationsEnabled: true,
    publisherConfirmationTrackingEnabled: true);                  // (1)
await using var channel = await connection.CreateChannelAsync(options);
await channel.QueueDeclareAsync(
    queue: "orders", durable: true, exclusive: false, autoDelete: false,
    arguments: new Dictionary<string, object?>
    {
        ["x-queue-type"] = "quorum"                               // (2)
    });
var properties = new BasicProperties
{
    Persistent = true,
    MessageId = Guid.NewGuid().ToString()
};
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await channel.BasicPublishAsync(
    exchange: "", routingKey: "orders", mandatory: true,
    basicProperties: properties, body: Encoding.UTF8.GetBytes("order-created"),
    cancellationToken: timeout.Token);                           // (3)
🧾 Code walkthrough
  1. (1) Confirmation tracking makes awaiting the publish observe its outcome; this is the version 7 async , replacing older IModel examples.
  2. (2) A durable queue is declared; fault tolerance depends on cluster topology and available replicas.
  3. (3) Mandatory publishing detects missing routes. With tracking, a return or nack fails the publish. A timeout leaves an uncertain outcome, so retries retain the message ID.
Publishing one message at a time is simple but limits ; bounded batching can help. Reuse connections and serialize access to a publishing channel. A confirm does not establish successful business processing. See the publisher-confirm guide.

#Acknowledgments, prefetch and dead lettering

When a delivers a message to a consumer, the consumer sends an ack (acknowledgment) after successful handling so the broker knows it can clear that delivery. If the consumer saves a database change and then crashes before the ack, the broker may deliver the message again; the handler must tolerate duplicates without repeating the effect. Prefetch limits delivered messages that are still unacknowledged; it is different from the number of handlers running at once.

// Inside a BackgroundService: channel and HandleAsync are application collaborators.
// Provision the queue and a bound DLQ first; configure its DLX through broker policy.
using RabbitMQ.Client;
using RabbitMQ.Client.Events;

await channel.BasicQosAsync(0, prefetchCount: 16, global: false); // (1)
var consumer = new AsyncEventingBasicConsumer(channel);
consumer.ReceivedAsync += async (_, delivery) =>
{
    byte[] body = delivery.Body.ToArray();                      // (2)
    try
    {
        await HandleAsync(body, stoppingToken);
    }
    catch (InvalidDataException)
    {
        await channel.BasicNackAsync(
            delivery.DeliveryTag, multiple: false, requeue: false); // (3)
        return;
    }
    await channel.BasicAckAsync(delivery.DeliveryTag, multiple: false); // (4)
};
await channel.BasicConsumeAsync("orders", autoAck: false, consumer: consumer);
await Task.Delay(Timeout.Infinite, stoppingToken);               // (5)
🧾 Code walkthrough
  1. (1) This configuration allows sixteen unacknowledged deliveries per consumer; tune distribution, and memory together.
  2. (2) Client-owned body memory has callback-bound lifetime; copy it before retaining it.
  3. (3) Permanent rejection disables requeue; it dead-letters only when a is configured, otherwise it can be discarded.
  4. (4) Acknowledge success on the delivery channel; multiple:false avoids acknowledging unfinished deliveries.
  5. (5) Keep the channel owner alive. Shutdown should cancel the consumer and drain in-flight handlers before disposal.

The fragment isolates permanent failures. Transient failures require explicit callback exception handling and bounded delayed retries or retry queues; an unhandled callback exception is not a retry policy. Closing a channel requeues unacknowledged deliveries, but unlimited immediate requeue creates a hot loop. Monitor the and provide controlled replay. See the client API guide.

#Kafka: logs, partitions and offsets

When several systems need to replay history, a queue that removes acknowledged messages may be the wrong model. Kafka partitions a topic into ordered logs, each with partition-local offsets. Reading does not remove records; time or size retention controls their lifetime, while compaction is a separate policy for retaining keyed state.

In a conventional consumer group, a partition has one active consumer within that group. Different groups read independently. Committing before processing can lose effects; committing afterwards can repeat them after a crash. The committed offset is a restart position, not proof that every external system was updated.

topic: orders
partition 0: offset 0 [order A v1]  offset 1 [order A v2] // (1)
partition 1: offset 0 [order B v1]
group invoicing: consumer X -> p0, consumer Y -> p1     // (2)
group analytics: consumer Z -> p0 and p1                // (3)
🧾 Code walkthrough
  1. (1) Use the order ID with a stable partitioner; changing the partition count can move keys. Ordering is per partition, not across the topic.
  2. (2) In this model, partitions bound parallel consumers within a group; extra consumers remain idle.
  3. (3) Analytics has independent offsets and can replay retained records.

Producer retries, -producer settings and in-flight limits affect order preservation. Parallel handling within a partition or skipping failed records can reorder business effects. See Kafka's design documentation.

#RabbitMQ, Kafka and Azure Service Bus

DimensionRabbitMQ queuesKafkaAzure Service Bus
ModelFlexible routing and per-message acknowledgmentsPartitioned retained logManaged enterprise queues, topics and subscriptions
After processingAcknowledgment removes the queued messageOffsets advance; records remain under retention policyComplete removes the message from the queue or subscription
Fan-outQueue per subscriber through exchangesConsumer group per subscriberSubscription per topic subscriber
OrderingSingle active consumer and sequential processing; retries require designWithin a partitionSessions for related message processing
Typical fitWork queues, commands and custom routingStreaming, replay and data pipelinesManaged Azure integration, and sessions

RabbitMQ also supports Streams; this comparison concerns traditional queues. Choose using replay, routing, , operational skills and cost rather than universal speed claims. Service Bus duplicate detection has bounded scope and time; it does not guarantee exactly-once business effects. See the Service Bus overview.

#Transactional outbox and inbox

A worked example
  1. An order is saved, but the process crashes before publishing OrderCreated. The order exists while shipping knows nothing about it.
  2. Save both the order and an message in the same local . A separate publisher sends pending messages and records progress.
  3. A crash after sending but before recording success can cause a repeat send. Consumers must tolerate duplicates: the outbox closes the save/publish gap, not automatically every duplicate external effect.

Saving and then publishing are separate writes: a crash between them loses the event, while publishing first can announce an order that never commits. Store the business change and outbox record in one database transaction, then dispatch later. At the consumer, persist the message ID and effect in one transaction.

// EF Core fragments; entities and serializer are application code.
await using var tx = await db.Database.BeginTransactionAsync(ct);
db.Orders.Add(order);
db.Outbox.Add(new OutboxMessage(messageId, Serialize(order))); // (1)
await db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);

// Dispatcher: claim a row safely before publishing; broker confirms required.
await PublishWithConfirmAsync(row.Id, row.Payload, ct);       // (2)
row.MarkSent();
await db.SaveChangesAsync(ct);

// Consumer uses a different DbContext and transaction.
await using var receiveTx = await db.Database.BeginTransactionAsync(ct);
db.Inbox.Add(new InboxMessage(consumerName, messageId));      // (3)
await db.SaveChangesAsync(ct);
await ApplyDatabaseEffectAsync(db, message, ct);             // (4)
await db.SaveChangesAsync(ct);
await receiveTx.CommitAsync(ct);
// Only now acknowledge the delivery.
🧾 Code walkthrough
  1. (1) Both writes commit or roll back together; outbox records need stable IDs, type, payload, timestamps and attempt state.
  2. (2) A crash after confirmation but before marking sent causes republishing. Multiple dispatchers need safe claiming, leases and bounded batches.
  3. (3) Enforce a unique constraint on consumer name and message ID. Handle that specific duplicate-key violation by rolling back and confirming a committed duplicate before acknowledgment; do not swallow other database errors.
  4. (4) Apply a database effect inside this transaction, rather than an external charge. Failure rolls back the inbox row too.
Check-then-insert alone cannot prevent concurrent duplicates. An inbox cannot make an external email or charge atomic; use provider keys, another outbox and reconciliation. Retain inbox records for the relevant replay window.

#Delivery guarantees, idempotency and ordering

GuaranteeConsequence
At-most-onceA message may be lost; that path does not redeliver.
At-least-onceRetries and redelivery can duplicate effects without deduplication or .
Exactly-once within a scopePossible with defined transactional guarantees, such as Kafka records and offsets in Kafka ; not automatically extended to or payment .

Assigning a fixed status may be idempotent but does not prevent a stale event from moving an order from Shipped back to Paid. Use per- versions or sequences, and define gap rejection, buffering or reconciliation.

In RabbitMQ, one queue with a single active consumer and sequential handling limits reordering; requeues, retries and multiple producers require further rules. Kafka orders within a partition, not across partitions. An ordered must stop later rows overtaking a failed row for the same order. Global order has a coordination cost.

#Sagas: choreography and orchestration

Reserving stock and collecting payment across services cannot use one local . A saga tracks local transactions and requests compensations such as release or refund after failure. Compensation is a new business action, rather than a magical rollback or erasure of a payment.

StyleCoordination and trade-off
ChoreographyServices react to events; simple for short flows, but control flow spreads and cycles can develop.
OrchestrationA coordinator persists state, issues commands and receives outcomes; the flow is explicit, but the coordinator needs persistence and recovery.
Pending -> StockReserved -> PaymentCaptured -> Confirmed // (1)
StockReserved + PaymentRejected -> ReleasingStock -> Cancelled
PaymentCaptured + FulfilmentFailed -> Refunding -> Refunded // (2)
🧾 Code walkthrough
  1. (1) Persist transitions and outgoing commands through an , with correlation IDs and step deadlines.
  2. (2) Refunds can fail too; use bounded retries, monitoring and manual recovery. Actions and compensations must tolerate repetition.

If require immediate enforcement, consider one transactional boundary before introducing a saga. Sagas do not supply ; explicit pending states and reservations help prevent races.

#Resilience and messaging frameworks

A slow dependency can consume available connections and execution capacity. Retries with jitter handle transient failures; timeouts bound waiting; circuit breakers temporarily reject calls after repeated failures; bulkheads isolate concurrency capacity. Do not retry validation failures or repeat unsafe charges without an contract.

// Package: Microsoft.Extensions.Http.Resilience; Polly v8-based integration.
using Microsoft.Extensions.Http.Resilience;

builder.Services.AddHttpClient("inventory")
    .AddStandardResilienceHandler(options =>
    {
        options.Retry.MaxRetryAttempts = 3;
        options.Retry.UseJitter = true;                   // (1)
        options.Retry.DisableForUnsafeHttpMethods();     // (2)
        options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(2);
        options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(10); // (3)
    });
🧾 Code walkthrough
  1. (1) The standard handler combines concurrency limiting, total timeout, retry, circuit breaker and attempt timeout. Jitter spreads retry attempts.
  2. (2) Disabling retries for unsafe methods is conservative; understand operation semantics before customizing it.
  3. (3) The total budget includes attempts and delays; a timeout does not prove the remote action did not happen.
Older approachModern approach
Polly v7 policy wrappers or hand-written retry loopsMicrosoft.Extensions.Http.Resilience using Polly v8 pipelines

MassTransit and Wolverine offer consumer pipelines, retries, durable messaging and saga facilities through their chosen integrations. Configure persistence, and failure handling explicitly; an in-memory outbox is not a durable . The HTTP handler protects outgoing HTTP, rather than defining consumer retries. See HTTP resilience, MassTransit outbox and Wolverine durability.

Interview questions

Check your reasoning

Why does this choice fit? When would it fail? What changes under higher load or a dependency failure?

17.1Why use asynchronous messaging instead of an HTTP call?Junior
Model answer

I use messaging when work can happen after the initiating , such as sending an order email. A durable queue separates service availability and buffers bursts so consumers can process at a sustainable rate. The trade-offs are , duplicate delivery and more complex operations. I keep synchronous calls when the caller needs an immediate decision, and expose a meaningful pending state when processing continues asynchronously. I also monitor message age rather than assuming an accepted message has completed.

17.2What is a message broker?Junior
Model answer

I describe a as infrastructure that accepts messages from producers and delivers them to consumers using routing, storage and acknowledgment rules. It lets the sender and receiver operate independently in time and can buffer temporary load differences. The guarantees depend on configuration, including persistence, and confirmations. I still need explicit message contracts, monitoring and failure handling in the application. A broker acceptance signal alone does not tell me whether the consumer successfully applied the business operation.

17.3What is the difference between a command and an event?Junior
Model answer

I use a command to request an action from a responsible owner, such as ReserveStock, and an event to announce a fact, such as OrderPlaced. An event can have several independent subscribers, while a command normally has one logical handler even when workers compete for it. This distinction keeps ownership clear and prevents publishers from depending on subscriber internals. I version contracts carefully and avoid including sensitive or unnecessary data simply because messaging makes distribution easy.

17.4What are RabbitMQ's main components?Mid
Model answer

I publish through a channel to an exchange with a routing key. Bindings describe which queues receive the message, and consumers receive deliveries from those queues. A connection is a long-lived network connection containing lighter channels, while a virtual host provides a logical boundary for topology and permissions. I distinguish routing from storage: the exchange routes and the queue holds pending messages. A dead-letter exchange also needs an appropriate binding and queue if failures must be retained for investigation.

17.5Explain direct, topic, fanout, headers and default exchanges.Mid
Model answer

I choose a direct exchange for exact routing-key matches and a topic exchange for dot-separated patterns, where star matches one word and hash matches zero or more. Fanout sends a copy to every bound queue and ignores the key. Headers exchanges evaluate attributes using all or any matching. The default exchange has an empty name and automatically binds each queue using its queue name. These routing choices determine recipients; competing consumers on one queue still share that queue's deliveries.

17.6How do consumer acknowledgments differ from publisher confirms?Mid
Model answer

I use publisher confirms to know that the accepted responsibility for a published message under its configured durability rules. Consumer acknowledgments report successful handling of a delivery and let the broker remove it from the queue. They protect different stages and neither replaces the other. I acknowledge only after committing the business effect. If the consumer crashes between commit and acknowledgment, the message can return, so I design for duplicates even when every publisher uses confirmations.

17.7What do prefetch, nack and a DLQ do?Mid
Model answer

I tune prefetch to bound unacknowledged deliveries and balance , memory and fair distribution. A negative acknowledgment rejects a delivery, either requeuing it or allowing dead lettering when configured. I avoid unlimited immediate requeue because a poison message can create a tight failure loop. Instead, I use bounded delayed retries for transient failures and a dead-letter queue for permanent failures. I monitor both backlog age and dead-letter growth, with an explicit investigation and replay process.

17.8How do Kafka partitions and consumer groups work?Mid
Model answer

I think of a Kafka topic as multiple ordered partition logs. In a conventional consumer group, each partition is assigned to one active consumer, so partition count bounds parallel consumers in that group. Other groups read independently with their own offsets. I use a stable entity key to keep related records together and preserve per-entity order, while accounting for partition-count changes. Reading does not remove records; retention determines whether a consumer can later replay them.

17.9When would you choose RabbitMQ, Kafka or Azure Service Bus?Mid
Model answer

I start with the consumption model and operational requirements. RabbitMQ queues fit commands, work distribution and flexible routing. Kafka fits retained event streams, independent readers and replay. Azure Service Bus fits managed enterprise messaging with queues, subscriptions, dead lettering and sessions in Azure. I compare ordering scope, recovery, retention, cost and team skills before choosing. I also specify the exact product features under discussion, since RabbitMQ Streams and extensions can change a simplistic comparison.

17.10What problem does the transactional outbox solve?Mid
Model answer

I use an to avoid the gap between committing business data and publishing its event. The application writes both the business change and an outbox row in the same database . A dispatcher later publishes with confirmation and records success. A crash after publishing but before recording success can cause another publication, so the message ID stays stable and consumers handle duplicates. I also plan safe row claiming, retries, ordering where required, and monitoring of the oldest unsent record.

17.11How do you implement an idempotent consumer safely?Senior
Model answer

I enforce a unique consumer-and-message key in an inbox and commit that record with the database effect in one . The unique constraint handles concurrent duplicates; a preliminary existence check alone is insufficient. I acknowledge only after commit, and distinguish the expected duplicate-key case from other database failures. External effects need their own contract or another durable delivery step, because a transaction cannot roll back a payment . I retain deduplication records for the supported replay window.

17.12Can you guarantee exactly-once processing?Senior
Model answer

I first define the boundary of the guarantee. A platform can provide exactly-once semantics within a specific scope, such as Kafka records and offsets, without covering a separate database or external payment provider. For a cross-system workflow, I usually combine at-least-once delivery with atomic inbox processing, unique business keys and provider keys. I describe the remaining failure windows explicitly and add reconciliation, rather than claiming that a option makes every business effect happen exactly once.

17.13How do you maintain message ordering?Senior
Model answer

I define ordering per business entity whenever possible, since global ordering limits concurrency. In Kafka I use a stable entity key and sequential processing within its partition. In RabbitMQ I can use a single active consumer with sequential handling, while designing retries and redeliveries carefully. The must preserve the same stream order so later events cannot overtake a failed earlier event. Consumers track versions or sequences and have an explicit policy for stale records and gaps.

17.14What is a saga, and how do choreography and orchestration differ?Senior
Model answer

I use a saga for a long-running workflow across boundaries, with local commits and compensating actions. Choreography lets participants react to events, while orchestration uses a coordinator to persist progress and issue commands. I prefer explicit orchestration when timeouts and recovery make the flow difficult to understand through events alone. Compensations are business actions that can fail and need and monitoring. A saga also does not provide , so reservations and pending states remain important.

17.15How do retry, timeout, circuit breaker and bulkhead work together?Mid
Model answer

I give the operation a total time budget, bound each attempt, and retry only transient failures where repeating the effect is safe. Exponential backoff with jitter prevents synchronized retry bursts. A circuit breaker rejects calls temporarily when a dependency is failing, then admits probes during recovery. A bulkhead limits concurrent work so one dependency cannot consume all capacity. In I can configure the standard resilience handler, while avoiding layered retry multiplication and treating consumer retries as a separate policy.

Test yourself

Quiz answers and completed-topic marks are saved in this browser; they do not sync across devices.

Score: 0 / 0

A consumer commits a change and crashes before ack. What should you expect?

The database commit and acknowledgment are separate operations.

Which topic binding matches order.paid.eg?

Hash matches zero or more words; star matches one.

A fanout exchange sends a copy to...

Copies go to queues; consumers sharing one queue compete.

What does an outbox atomically save with the order?

Atomicity is local to the database; publication follows later.

Six partitions in a conventional Kafka group allow how many active partition owners?

Each partition has one active consumer within that group model.

A RabbitMQ nack with requeue=false retains the failure when...

Without a dead-letter destination, the message can be discarded.

A payment request times out. Which statement is correct?

The action may have succeeded while its response was lost.

What is a saga compensation?

It is a new action with failure paths and retries, not a rewind.

Which exchange uses format=pdf and region=eg attributes for routing?

Headers exchanges match attributes using all or any.

Which signal proves that the consumer completed its business effect?

acceptance differs from completed consumer processing.