Blog
Mert Özen's blog posts on software development, backend, frontend, and DevOps.
Getting Started with Message Queues in .NET: RabbitMQ, Azure Service Bus and Kafka Clients, Which One When
RabbitMQ for flexible routing and per-message ack/retry, Azure Service Bus for a managed service on Azure, Kafka for high volume and replay; the series starts with raw clients and moves on to abstractions. The three brokers' models, a practical comparison table, a program sending and reading the same OrderPlaced with three clients, the Docker Compose environment and a note on Amazon SQS/SNS.
First Producer and Consumer with RabbitMQ.Client 7: Async API, Connection/Channel Lifecycle and Manual Ack
Keep one IConnection for the application's lifetime, never share an IChannel across threads, verify publishes with publisher confirmations and mandatory, and run the consumer inside a BackgroundService with autoAck=false and prefetch. Real output shows a prefetch-less consumer pulling 498 messages into memory, autoAck losing 17 messages, automatic recovery and graceful shutdown.
RabbitMQ Topology in Code: Exchange Types, Bindings, Quorum Queues and Streams
Use quorum queues and topic exchanges for durable business messages, declare the topology idempotently at startup, and leave limits that may change (TTL, max-length, delivery-limit, DLX) to a policy instead of queue arguments. Real output: nine copies from four routing keys, a PRECONDITION_FAILED error, fanout cache invalidation, stream replay and reject-publish.
Azure Service Bus .NET SDK: Queues, Topics/Subscriptions, Sessions and ServiceBusProcessor
Register ServiceBusClient as a singleton with AddAzureClients, use managed identity instead of a connection string in Azure, run ServiceBusProcessor with AutoCompleteMessages=false and lock renewal, and use sessions for ordered work. Real emulator output: lock loss, three attempts hitting the DLQ in one second, filters, sessions, scheduled messages and duplicate detection.
Kafka in .NET with Confluent.Kafka: Producers, Consumer Groups, Offset Commits and Rebalancing
Keep the producer a singleton, use the key for ordering, Acks=All and idempotence, and Flush before dispose; in the consumer set EnableAutoOffsetStore=false, call StoreOffset after processing and write poison messages to a dead-letter topic. Real output: default auto commit skipping a half-processed message, duplicates after kill -9, eager rebalance, KIP-848 and consumer lag.
Message Contracts: Serialization, Versioning and Backward Compatibility
Keep contracts as records separate from domain entities, evolve them only by adding optional fields, turn on RespectRequiredConstructorParameters and RespectNullableAnnotations in System.Text.Json, and carry the version in a type header. Real output: with default settings a rename becomes a 0 TL invoice, a new enum value breaks old consumers, and Schema Registry rejects an incompatible schema with 409.
Messaging with MassTransit: Consumers, Publish/Send and Broker Topology — v9 Licensing and Alternatives
MassTransit takes over connections, acks, serialization and topology, reducing a consumer to a single IConsumer<T>; the price is its own exchange conventions, an envelope format and, in v9, a commercial licence. We move Billing to MassTransit 8.5.12, show publish vs send, the resulting RabbitMQ topology and the envelope, see v9 refuse to start without a licence, and build a decision table across v8, Wolverine, Rebus, NServiceBus and Brighter.
Retry and Redelivery in Consumers: Immediate, Delayed and Exponential Backoff
Handle transient failures with a short in-memory retry and longer waits through the broker; never retry permanent failures, send them to the DLQ. Real output shows nack-requeue looping 8036 times in 5 seconds on RabbitMQ 4.4, TTL + DLX wait queues, the quorum queue's built-in delayed retry, scheduled retry in Service Bus, Kafka retry topics and MassTransit UseMessageRetry.
Managing Dead-Letter Queues: Finding, Inspecting and Reprocessing Poison Messages
Run the DLQ like a work queue: read why each message failed from its headers, alert on growth, and after the fix send it back with a dry run, filters, a cap and your own replay counter. Real output shows RabbitMQ's x-death count not advancing on a client republish, Service Bus DLQ reason codes, a wrong original-topic bug in a Kafka DLT and MassTransit _error headers.
Idempotent Consumers and the Inbox: When the Same Message Arrives Twice
Stop a message from being processed twice with a unique constraint in the database, not with a check: write an inbox row keyed by MessageId in the same transaction as the business data, or apply ON CONFLICT to the business key. A real run of check-then-act issuing two invoices, inbox and upsert on PostgreSQL, side effects, dedupe cleanup and the MassTransit inbox.
Transactional Outbox in .NET: EF Core, a BackgroundService Relay and the MassTransit/Wolverine Outbox
Write the message to an outbox table in the same transaction as the business data instead of to the broker, and let a BackgroundService relay pick rows with FOR UPDATE SKIP LOCKED and publish them with confirms. A real run of two instances publishing 200 messages exactly once and returning 201 while the broker is down; why the relay is at-least-once and unordered; MassTransit bus outbox, Wolverine and CDC.
Observability in Messaging: OpenTelemetry Trace Context, Queue Depth and Consumer Lag
Listen to RabbitMQ.Client 7's built-in ActivitySource and write the traceparent into Kafka headers yourself with a propagator; an order's journey through the API, RabbitMQ, Billing, Kafka and Notification then shows up as five spans in one trace. MessageId and CorrelationId in the log scope, the messaging.process.duration histogram, cardinality, queue depth and Kafka consumer lag, with real output.