Message Broker functions

A message broker is a process that accepts messages, stores them (at least briefly), and delivers them to consumers. You use one to decouple when work happens from who asked. It is not a database, not a REST bus, and not “async” by itself — your handlers still have to be correct under retry.

Jobs it is good at

  • Buffering: producer is fast, consumer is slow.
  • Fan-out: one event, many independent consumers (email, search index, analytics).
  • Temporal decoupling: the other service is down; the message waits.

Delivery words that matter

  • At-most-once: may drop. Almost never what you want for money or inventory.
  • At-least-once: the contract to design for. SQS standard queues deliver at-least-once. Kafka and RabbitMQ do too when the consumer acks only after the handler succeeds. RabbitMQ auto-ack is at-most-once: a crash after delivery drops the message. Kafka’s default auto-commit is roughly at-least-once for a synchronous handler and can still repeat work. Handlers must be idempotent.
  • Exactly-once: real for a Kafka consume-transform-produce transaction. It does not cover the database write or the email in that same handler. Everywhere else, “effectively once” means at-least-once plus an idempotent write or an inbox.

Competing consumers share a queue (scale workers). Pub/sub copies to each subscription. Mixing them up is how you process a payment twice or never.

Outbox

Do not SaveChanges() then Publish() on two connections. Commit the entity and an outbox row together; a relay publishes. The inverse (inbox) records “we already handled message id X.”

Poison messages

A handler that always throws will block a partition or fill a retry loop. After N failures, dead-letter the message with the exception. Alert on DLQ depth. Do not delete silently.

Example

The order and the outbox row commit together. A relay publishes the row and marks it sent.

BEGIN;
INSERT INTO orders (id, total) VALUES ($1, $2);
INSERT INTO outbox (id, type, payload) VALUES ($3, 'OrderPlaced', $4);
COMMIT;

What breaks it

The process can die after the save and before the publish. The order exists and the rest of the system never hears about it. Use the transaction above.

await db.SaveChangesAsync(ct);
await bus.PublishAsync(new OrderPlaced(order.Id), ct);

Pitfalls

  • Using the broker as a shared database (querying “the queue” for state).
  • Huge payloads — see large messages.
  • Chatty request/response over a queue when HTTP would be a timeout you understand.