<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged rabbitmq</title>
<link>https://coderlegion.com/tag/rabbitmq</link>
<description>Powered by Question2Answer</description>
<item>
<title>The Heart of B3: Building the Matching Engine with RabbitMQ, Redis and Spring Boot</title>
<link>https://coderlegion.com/17341/the-heart-of-b3-building-the-matching-engine-with-rabbitmq-redis-and-spring-boot</link>
<description>&lt;p&gt;Hello everyone!&lt;/p&gt;
&lt;p&gt;Continuing the &lt;strong&gt;My Broker B3&lt;/strong&gt; series, we've reached one of the most anticipated components of the ecosystem: the &lt;strong&gt;B3 Matching Engine API&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In the previous post, we built &lt;code&gt;b3-market-sync-api&lt;/code&gt; which synchronizes real prices from brapi.dev and stores them in Redis. Now it's time to use that data — this is the service that simulates the stock exchange itself, deciding whether a buy or sell order will be &lt;strong&gt;executed or rejected&lt;/strong&gt; based on the prices we just put in cache.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-what-is-the-matching-engine-&quot;&gt;️ What is the Matching Engine?&lt;/h2&gt;
&lt;p&gt;In the real world, when you place a buy order through your broker, it gets sent to B3, which checks whether there's a counterpart willing to trade at that price. This process is called &lt;strong&gt;matching&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In our simulator, the &lt;code&gt;b3-matching-engine-api&lt;/code&gt; reproduces this logic in a simplified way:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Intake&lt;/strong&gt;: Receives the order from the broker via RabbitMQ (queue &lt;code&gt;mq-broker-to-b3&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Price&lt;/strong&gt;: Fetches the current asset price from Redis (&lt;code&gt;market:price:{TICKER}&lt;/code&gt;) — data injected by the Market Sync we built previously&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decision&lt;/strong&gt;: Applies the matching rule&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Persistence&lt;/strong&gt;: Records the result in PostgreSQL&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Notification&lt;/strong&gt;: Returns the result to the broker via RabbitMQ (queue &lt;code&gt;mq-b3-to-broker&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;[Broker] ──RabbitMQ──▶ [mq-broker-to-b3] ──▶ [Matching Engine]
                                                      │
                                             Redis: market:price:{TICKER}
                                             (fed by Market Sync ⬆️)
                                                      │
                                         ┌────────────┴────────────┐
                                      FILLED                   REJECTED
                                         │                         │
                                   PostgreSQL                 PostgreSQL
                                         │                         │
                               [mq-b3-to-broker] ◀────────────────┘
                                         │
                                      [Broker]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;mvp-focus&quot;&gt;MVP Focus&lt;/h2&gt;
&lt;p&gt;Before diving into the code, same disclaimer as previous posts: &lt;strong&gt;we're building the foundation&lt;/strong&gt;. The goal is to have an end-to-end flow working with enough robustness to validate the POC.&lt;/p&gt;
&lt;p&gt;In this phase, I prioritized:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;End-to-end flow working correctly&lt;/li&gt;
&lt;li&gt;Proper failure handling (no orders silently disappearing)&lt;/li&gt;
&lt;li&gt;Dead Letter Queue for messages that fail during processing&lt;/li&gt;
&lt;li&gt;REST API for querying execution history&lt;/li&gt;
&lt;li&gt;Swagger documentation&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-tech-stack&quot;&gt;️ Tech Stack&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Technology &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Usage&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Java 21&lt;/strong&gt; + &lt;strong&gt;Spring Boot 3.5&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Service core&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Spring RabbitMQ&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Order intake and result notification&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Spring Data Redis&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Real-time price lookup&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Spring Data JPA&lt;/strong&gt; + &lt;strong&gt;PostgreSQL&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Execution persistence&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Flyway&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Database schema versioning&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;SpringDoc OpenAPI&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Swagger UI documentation&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-implementation-pillars&quot;&gt;️ Implementation Pillars&lt;/h2&gt;
&lt;h3 id=&quot;1-rabbitmq-configuration&quot;&gt;1. RabbitMQ Configuration&lt;/h3&gt;
&lt;p&gt;A critical lesson learned here: &lt;strong&gt;never rely on queues already existing in the broker&lt;/strong&gt;. If the application starts before RabbitMQ has the queues created, the consumer fails on startup.&lt;/p&gt;
&lt;p&gt;The solution is to declare all infrastructure beans directly in Spring — it ensures queues, exchanges and bindings exist before any message is processed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Bean
public DirectExchange exchange() {
    return new DirectExchange(exchangeName);
}

@Bean
public Queue queueIn() {
    return QueueBuilder.durable(queueIn)
            .withArgument(&quot;x-dead-letter-exchange&quot;, dlxName)
            .withArgument(&quot;x-dead-letter-routing-key&quot;, dlqName)
            .build();
}

@Bean
public Binding bindingQueueIn(Queue queueIn, DirectExchange exchange) {
    return BindingBuilder.bind(queueIn).to(exchange).with(routingKey);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note the &lt;code&gt;withArgument&lt;/code&gt; calls — they configure the &lt;strong&gt;Dead Letter Queue&lt;/strong&gt; directly in the main queue declaration. Any message that fails processing is automatically redirected.&lt;/p&gt;
&lt;h3 id=&quot;2-the-matching-logic&quot;&gt;2. The Matching Logic&lt;/h3&gt;
&lt;p&gt;The heart of the service. The rule is straightforward:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;BUY&lt;/strong&gt;: If the price the user accepts to pay &amp;gt;= market price → &lt;code&gt;FILLED&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SELL&lt;/strong&gt;: If the price the user wants to receive &amp;lt;= market price → &lt;code&gt;FILLED&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Otherwise → &lt;code&gt;REJECTED&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private static boolean isCanExecute(OrderEventDTO order, BigDecimal marketPrice) {
    if (SideStatus.BUY.name().equalsIgnoreCase(order.getSide())) {
        return order.getPrice().compareTo(marketPrice) &amp;gt;= 0;
    } else if (SideStatus.SELL.name().equalsIgnoreCase(order.getSide())) {
        return order.getPrice().compareTo(marketPrice) &amp;lt;= 0;
    }
    return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3-failure-handling-no-silent-order-loss&quot;&gt;3. Failure Handling — No Silent Order Loss&lt;/h3&gt;
&lt;p&gt;A silent bug I identified: when the ticker wasn't in Redis, the method simply returned without doing anything. The broker would wait for a response that never came.&lt;/p&gt;
&lt;p&gt;The fix was simple but critical — any failure must notify the broker:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;if (marketDataOpt.isEmpty()) {
    log.warn(&quot;Price for ticker {} not found in Redis. Rejecting order {}&quot;,
             order.getTicker(), order.getOrderId());
    OrderResponseEvent response = new OrderResponseEvent(
        order.getOrderId(), ExecutionStatus.REJECTED.name(), BigDecimal.ZERO);
    orderProducer.sendToBroker(response);
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt; This is exactly why we built &lt;code&gt;b3-market-sync-api&lt;/code&gt; first. If Redis doesn't have the price, the order is immediately rejected — correct and predictable behavior.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;4-transactional-guarantee&quot;&gt;4. Transactional Guarantee&lt;/h3&gt;
&lt;p&gt;Another critical point: persisting to PostgreSQL and publishing to RabbitMQ are two distinct operations. If the database saves but the publish fails, the execution is recorded but the broker is never notified — silent inconsistency.&lt;/p&gt;
&lt;p&gt;The solution was adding &lt;code&gt;@Transactional&lt;/code&gt; to the method that orchestrates both operations:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Transactional
private void saveAndNotify(OrderEventDTO order, BigDecimal price, ExecutionStatus status) {
    // 1. Persists in PostgreSQL
    OrderExecution execution = OrderExecution.builder()
            .orderId(order.getOrderId())
            .ticker(order.getTicker())
            .side(SideStatus.valueOf(order.getSide()))
            .quantity(order.getQuantity())
            .executedPrice(price)
            .status(status)
            .build();
    repository.save(execution);

    // 2. Notifies the Broker
    OrderResponseEvent response = new OrderResponseEvent(
        order.getOrderId(), status.name(), price);
    orderProducer.sendToBroker(response);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;5-dead-letter-queue&quot;&gt;5. Dead Letter Queue&lt;/h3&gt;
&lt;p&gt;For messages that fail in the consumer (unexpected processing error), we configured a full DLQ setup:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Bean
public DirectExchange deadLetterExchange() {
    return new DirectExchange(dlxName);
}

@Bean
public Queue deadLetterQueue() {
    return QueueBuilder.durable(dlqName).build();
}

@Bean
public Binding bindingDlq(Queue deadLetterQueue, DirectExchange deadLetterExchange) {
    return BindingBuilder.bind(deadLetterQueue)
            .to(deadLetterExchange)
            .with(dlqName);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the consumer, we rethrow the exception so RabbitMQ knows the message failed and routes it to the DLQ:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@RabbitListener(queues = &quot;${app.rabbitmq.queue-in}&quot;)
public void receiveOrder(OrderEventDTO event) {
    log.info(&quot;New order received: ID {} | Ticker {} | Side {}&quot;,
            event.getOrderId(), event.getTicker(), event.getSide());
    try {
        matchingService.process(event);
    } catch (Exception e) {
        log.error(&quot;Failed to process order {}: {}&quot;, event.getOrderId(), e.getMessage(), e);
        throw e; // RabbitMQ routes to DLQ
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;6-rest-api-swagger&quot;&gt;6. REST API + Swagger&lt;/h3&gt;
&lt;p&gt;Since the service exposes query endpoints, I configured &lt;strong&gt;Swagger UI&lt;/strong&gt; from the start:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Bean
public OpenAPI customOpenAPI() {
    return new OpenAPI()
            .info(new Info()
                    .title(&quot;B3 Matching Engine API&quot;)
                    .version(&quot;1.0.0&quot;)
                    .description(&quot;Simulates the B3 stock exchange matching engine...&quot;));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Available endpoints:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Method &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Endpoint &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Description&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;GET &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;/api/v1/executions&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;List all executions&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;GET &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;/api/v1/executions/order/{orderId}&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Find by order ID&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;GET &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;/api/v1/executions/ticker/{ticker}&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Find by ticker&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;GET &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;/api/v1/executions/status/{status}&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Find by status&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-validating-the-execution&quot;&gt;✅ Validating the Execution&lt;/h2&gt;
&lt;p&gt;With the application running locally, all components started correctly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Flyway applied the &lt;code&gt;order_executions&lt;/code&gt; table migration&lt;/li&gt;
&lt;li&gt;✅ Hibernate validated the schema&lt;/li&gt;
&lt;li&gt;✅ RabbitMQ connected with queues and DLQ declared&lt;/li&gt;
&lt;li&gt;✅ Tomcat running on port &lt;code&gt;8091&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;✅ Swagger UI accessible at &lt;code&gt;http://localhost:8091/swagger-ui.html&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-s-next-&quot;&gt;What's Next?&lt;/h2&gt;
&lt;p&gt;With &lt;code&gt;b3-market-sync-api&lt;/code&gt; feeding Redis and &lt;code&gt;b3-matching-engine-api&lt;/code&gt; ready to process orders, the entire B3 side is operational.&lt;/p&gt;
&lt;p&gt;The next step is building the &lt;strong&gt;broker-order-api&lt;/strong&gt; — the orchestrator on the broker side that will manage the complete order lifecycle:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Receive the buy/sell intent from the user&lt;/li&gt;
&lt;li&gt;Validate the ticker via REST against &lt;code&gt;broker-asset-api&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Save the order as &lt;code&gt;PENDING&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Publish the event to Kafka&lt;/li&gt;
&lt;li&gt;Send the order to the Matching Engine via RabbitMQ&lt;/li&gt;
&lt;li&gt;Consume the feedback and update the final status&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Once that service is ready, we'll have the first complete end-to-end flow running in the ecosystem.&lt;/p&gt;
&lt;p&gt;Got any questions about the matching logic or RabbitMQ DLQ configuration? Drop them in the comments!&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;about-the-series&quot;&gt;About the Series&lt;/h2&gt;
&lt;p&gt;⬅️ Previous Post: &lt;a rel=&quot;nofollow&quot; href=&quot;#&quot;&gt;Syncing the Real Market: Consuming Brapi and Feeding Redis with Spring Boot&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; Series Index: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/rvneto/series-roadmap-building-a-stock-brokerage-simulator-with-microservices-kgh&quot;&gt;Series Roadmap&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/roberto-de-vargas/&quot;&gt;LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rvneto/trading-b3-matching-engine&quot;&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/17341/the-heart-of-b3-building-the-matching-engine-with-rabbitmq-redis-and-spring-boot</guid>
<pubDate>Sun, 10 May 2026 21:29:07 +0000</pubDate>
</item>
<item>
<title>Understanding RabbitMQ in Real-World .NET Systems: Why, When, and How to Use It</title>
<link>https://coderlegion.com/10115/understanding-rabbitmq-in-real-world-net-systems-why-when-and-how-to-use-it</link>
<description>&lt;p&gt;Most teams don’t start with RabbitMQ.&lt;/p&gt;
&lt;p&gt;They add it when something begins to hurt.&lt;/p&gt;
&lt;p&gt;Requests become slow.&lt;br&gt;
Failures spread across services.&lt;br&gt;
One small issue takes down multiple parts of the system.&lt;/p&gt;
&lt;p&gt;That’s usually when RabbitMQ enters the picture.&lt;/p&gt;
&lt;p&gt;This article explains what RabbitMQ actually solves, how to think about it from a senior-engineer perspective, and includes simple .NET examples to make the ideas concrete.&lt;/p&gt;
&lt;p&gt;No heavy theory.&lt;br&gt;
No marketing language.&lt;br&gt;
Just practical understanding.&lt;/p&gt;
&lt;p&gt;The Real Problem RabbitMQ Solves&lt;/p&gt;
&lt;p&gt;RabbitMQ solves tight coupling between systems.&lt;/p&gt;
&lt;p&gt;Without a message queue, systems usually communicate like this:&lt;/p&gt;
&lt;p&gt;“Do this now, and I’ll wait for the result.”&lt;/p&gt;
&lt;p&gt;That works—until it doesn’t.&lt;/p&gt;
&lt;p&gt;With RabbitMQ, communication changes to:&lt;/p&gt;
&lt;p&gt;“Here’s a message. Process it when you’re ready.”&lt;/p&gt;
&lt;p&gt;This small shift has a big impact:&lt;br&gt;
•Users don’t wait&lt;br&gt;
•Failures don’t spread&lt;br&gt;
•Systems become more resilient&lt;/p&gt;
&lt;p&gt;RabbitMQ creates space between components, and that space matters.&lt;/p&gt;
&lt;p&gt;A Simple Real-World Scenario&lt;/p&gt;
&lt;p&gt;Imagine a .NET API where a user places an order.&lt;/p&gt;
&lt;p&gt;Behind the scenes, you need to:&lt;br&gt;
•send a confirmation email&lt;br&gt;
•update inventory&lt;br&gt;
•notify another service&lt;/p&gt;
&lt;p&gt;If everything happens synchronously:&lt;br&gt;
•the API gets slower&lt;br&gt;
•one failure breaks the entire flow&lt;br&gt;
•retries become complicated&lt;/p&gt;
&lt;p&gt;With RabbitMQ:&lt;br&gt;
•the order is accepted immediately&lt;br&gt;
•background work happens later&lt;br&gt;
•each task runs independently&lt;/p&gt;
&lt;p&gt;The user moves on.&lt;br&gt;
The system stays calm.&lt;/p&gt;
&lt;p&gt;How RabbitMQ Works (Without Jargon)&lt;/p&gt;
&lt;p&gt;At a high level, the flow looks like this:&lt;/p&gt;
&lt;p&gt;Producer → Exchange → Queue → Consumer&lt;/p&gt;
&lt;p&gt;In practice:&lt;br&gt;
•Your .NET API publishes a message&lt;br&gt;
•RabbitMQ stores it safely&lt;br&gt;
•A background service consumes and processes it&lt;/p&gt;
&lt;p&gt;The producer does not need to know:&lt;br&gt;
•who consumes the message&lt;br&gt;
•how long it takes&lt;br&gt;
•whether it fails temporarily&lt;/p&gt;
&lt;p&gt;That decoupling is the real value.&lt;/p&gt;
&lt;p&gt;Publishing a Message in .NET (Simple Example)&lt;/p&gt;
&lt;p&gt;Here’s a minimal C# example using RabbitMQ.Client.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var factory = new ConnectionFactory
{
    HostName = &quot;localhost&quot;
};

using var connection = factory.CreateConnection();
using var channel = connection.CreateModel();

channel.QueueDeclare(
    queue: &quot;order.created&quot;,
    durable: true,
    exclusive: false,
    autoDelete: false,
    arguments: null
);

var message = &quot;Order 123 created&quot;;
var body = Encoding.UTF8.GetBytes(message);

channel.BasicPublish(
    exchange: &quot;&quot;,
    routingKey: &quot;order.created&quot;,
    basicProperties: null,
    body: body
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What matters here is not the syntax.&lt;/p&gt;
&lt;p&gt;What matters is this:&lt;br&gt;
 The API sends a message and continues its work.&lt;/p&gt;
&lt;p&gt;Consuming Messages in .NET (Background Worker)&lt;/p&gt;
&lt;p&gt;Now let’s look at a simple consumer.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var factory = new ConnectionFactory
{
    HostName = &quot;localhost&quot;
};

using var connection = factory.CreateConnection();
using var channel = connection.CreateModel();

channel.QueueDeclare(
    queue: &quot;order.created&quot;,
    durable: true,
    exclusive: false,
    autoDelete: false,
    arguments: null
);

var consumer = new EventingBasicConsumer(channel);

consumer.Received += (sender, eventArgs) =&amp;gt;
{
    var body = eventArgs.Body.ToArray();
    var message = Encoding.UTF8.GetString(body);

    Console.WriteLine($&quot;Processing: {message}&quot;);

    // Send email, update inventory, etc.
};

channel.BasicConsume(
    queue: &quot;order.created&quot;,
    autoAck: true,
    consumer: consumer
);

Console.ReadLine();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This consumer:&lt;br&gt;
•waits patiently&lt;br&gt;
•processes messages when they arrive&lt;br&gt;
•can be scaled independently&lt;/p&gt;
&lt;p&gt;Why This Design Works Well&lt;/p&gt;
&lt;p&gt;This pattern gives you:&lt;br&gt;
•faster user responses&lt;br&gt;
•fewer cascading failures&lt;br&gt;
•better control over background tasks&lt;/p&gt;
&lt;p&gt;If email sending fails:&lt;br&gt;
•the order is still saved&lt;br&gt;
•the message can be retried&lt;br&gt;
•users are not blocked&lt;/p&gt;
&lt;p&gt;That’s a real improvement, not just an architectural one.&lt;/p&gt;
&lt;p&gt;When RabbitMQ Is a Good Choice&lt;/p&gt;
&lt;p&gt;RabbitMQ works best when:&lt;br&gt;
•work can be asynchronous&lt;br&gt;
•reliability matters more than speed&lt;br&gt;
•services should stay independent&lt;br&gt;
•traffic comes in spikes&lt;/p&gt;
&lt;p&gt;Common use cases:&lt;br&gt;
•email and notification systems&lt;br&gt;
•background jobs&lt;br&gt;
•event-driven workflows&lt;br&gt;
•system integrations&lt;/p&gt;
&lt;p&gt;When RabbitMQ Is the Wrong Choice&lt;/p&gt;
&lt;p&gt;RabbitMQ is not always the answer.&lt;/p&gt;
&lt;p&gt;Avoid it when:&lt;br&gt;
•the system is very small&lt;br&gt;
•everything must be real-time&lt;br&gt;
•the team can’t support extra infrastructure&lt;/p&gt;
&lt;p&gt;Adding a message broker adds responsibility.&lt;br&gt;
If the problem is simple, keep the solution simple.&lt;/p&gt;
&lt;p&gt;Common Mistakes in Real Projects&lt;/p&gt;
&lt;p&gt;Some mistakes show up again and again:&lt;br&gt;
•Treating queues like databases&lt;br&gt;
•Ignoring retries and dead-letter queues&lt;br&gt;
•Writing consumers that aren’t idempotent&lt;br&gt;
•Adding async everywhere without reason&lt;/p&gt;
&lt;p&gt;RabbitMQ does not remove complexity.&lt;br&gt;
It moves complexity to where it belongs.&lt;/p&gt;
&lt;p&gt;RabbitMQ Is a Design Decision, Not a Tool Choice&lt;/p&gt;
&lt;p&gt;Senior engineers don’t choose RabbitMQ because it’s popular.&lt;/p&gt;
&lt;p&gt;They choose it because:&lt;br&gt;
•boundaries are clear&lt;br&gt;
•failures are contained&lt;br&gt;
•systems become easier to reason about&lt;/p&gt;
&lt;p&gt;RabbitMQ helps systems talk without depending on each other’s timing or health.&lt;/p&gt;
&lt;p&gt;That separation is powerful.&lt;/p&gt;
&lt;p&gt;Final Thoughts&lt;/p&gt;
&lt;p&gt;RabbitMQ is quiet technology.&lt;/p&gt;
&lt;p&gt;It doesn’t impress in demos.&lt;br&gt;
It doesn’t make headlines.&lt;/p&gt;
&lt;p&gt;But over time, it:&lt;br&gt;
•reduces incidents&lt;br&gt;
•improves reliability&lt;br&gt;
•makes systems easier to operate&lt;/p&gt;
&lt;p&gt;That’s why it stays.&lt;/p&gt;
&lt;p&gt;Good systems are not fast because they rush.&lt;br&gt;
They’re fast because they don’t block each other.&lt;/p&gt;
&lt;p&gt;That’s the real lesson RabbitMQ teaches.&lt;/p&gt;
&lt;p&gt;Thanks for reading &lt;/p&gt;
&lt;p&gt;If you’re using RabbitMQ:&lt;br&gt;
•What problem did it solve for you?&lt;br&gt;
•What lesson did you learn the hard way?&lt;/p&gt;
&lt;p&gt;Let’s discuss in the comments.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/10115/understanding-rabbitmq-in-real-world-net-systems-why-when-and-how-to-use-it</guid>
<pubDate>Wed, 21 Jan 2026 05:38:26 +0000</pubDate>
</item>
<item>
<title>MassTransit in ASP.NET Core: A Practical Guide to Event-Driven .NET</title>
<link>https://coderlegion.com/5749/masstransit-in-asp-net-core-a-practical-guide-to-event-driven-net</link>
<description>&lt;p&gt;MassTransit in ASP.NET Core: A Practical Guide to Event-Driven .NET&lt;br&gt;
Intro&lt;/p&gt;
&lt;p&gt;Event-driven architectures help teams decouple services, scale independently, and handle failures gracefully. MassTransit is a mature, open-source library that makes message-based workflows in .NET straightforward. This guide shows a minimal but production-ready setup in ASP.NET Core with RabbitMQ.&lt;/p&gt;
&lt;p&gt;1) Install packages&lt;/p&gt;
&lt;p&gt;From your Web API project:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dotnet add package MassTransit
dotnet add package MassTransit.RabbitMQ
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(For Azure Service Bus use MassTransit.Azure.ServiceBus.Core instead.)&lt;/p&gt;
&lt;p&gt;2) Define a message contract&lt;/p&gt;
&lt;p&gt;Contracts should be versionable and live in a shared project.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface SubmitOrder
{
    Guid OrderId { get; }
    string CustomerId { get; }
    DateTime Timestamp { get; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3) Create a consumer&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using MassTransit;

public sealed class SubmitOrderConsumer : IConsumer&amp;lt;SubmitOrder&amp;gt;
{
    public async Task Consume(ConsumeContext&amp;lt;SubmitOrder&amp;gt; context)
    {
        var msg = context.Message;
        // Your domain logic here (idempotent!)
        Console.WriteLine($&quot;Received SubmitOrder {msg.OrderId} for {msg.CustomerId}&quot;);
        await Task.CompletedTask;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;4) Configure MassTransit in Program.cs (.NET 9 minimal hosting)&lt;/p&gt;
&lt;p&gt;RabbitMQ example with retries and health checks.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using MassTransit;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddMassTransit(cfg =&amp;gt;
{
    cfg.SetKebabCaseEndpointNameFormatter();

    cfg.AddConsumer&amp;lt;SubmitOrderConsumer&amp;gt;(c =&amp;gt;
    {
        // optional: configure consumer-level retry, etc.
    });

    cfg.UsingRabbitMq((context, bus) =&amp;gt;
    {
        bus.Host(&quot;rabbitmq&quot;, h =&amp;gt;
        {
            h.Username(&quot;guest&quot;);
            h.Password(&quot;guest&quot;);
        });

        bus.ReceiveEndpoint(&quot;submit-order-queue&quot;, e =&amp;gt;
        {
            e.ConfigureConsumeTopology = false; 
            e.ConfigureConsumer&amp;lt;SubmitOrderConsumer&amp;gt;(context);

            e.UseMessageRetry(r =&amp;gt; r.Interval(3, TimeSpan.FromSeconds(5)));
            e.PrefetchCount = 16;
            e.ConcurrentMessageLimit = 8;
        });
    });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;builder.Services.AddHealthChecks();&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var app = builder.Build();
app.MapHealthChecks(&quot;/health&quot;);
app.MapGet(&quot;/&quot;, () =&amp;gt; &quot;OK&quot;);
app.Run();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Docker tip&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you run RabbitMQ locally via Docker:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --hostname rabbit \
  -p 5672:5672 -p 15672:15672 \
  --name rabbitmq rabbitmq:3-management
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;UI is at &lt;a rel=&quot;nofollow&quot; href=&quot;http://localhost:15672&quot;&gt;http://localhost:15672&lt;/a&gt; (guest/guest).&lt;/p&gt;
&lt;p&gt;5) Publish a message (from a Controller or Service)&lt;/p&gt;
&lt;p&gt;Inject IPublishEndpoint for pub/sub or ISendEndpointProvider for point-to-point.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using MassTransit;

public sealed class OrderAppService
{
    private readonly IPublishEndpoint _publish;
    public OrderAppService(IPublishEndpoint publish) =&amp;gt; _publish = publish;

    public Task SubmitAsync(Guid orderId, string customerId)
        =&amp;gt; _publish.Publish&amp;lt;SubmitOrder&amp;gt;(new
        {
            OrderId = orderId,
            CustomerId = customerId,
            Timestamp = DateTime.UtcNow
        });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or via an endpoint (send to a specific queue):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public sealed class OrderSender
{
    private readonly ISendEndpointProvider _send;
    public OrderSender(ISendEndpointProvider send) =&amp;gt; _send = send;

    public async Task SendAsync(Guid orderId, string customerId)
    {
        var endpoint = await _send.GetSendEndpoint(new Uri(&quot;queue:submit-order-queue&quot;));
        await endpoint.Send&amp;lt;SubmitOrder&amp;gt;(new { OrderId = orderId, CustomerId = customerId, Timestamp = DateTime.UtcNow });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;6) Error handling, retries, and observability&lt;/p&gt;
&lt;p&gt;Retries: use UseMessageRetry on endpoints or the bus. Prefer bounded retries with intervals or exponential backoff.&lt;/p&gt;
&lt;p&gt;Poison messages: failed messages after retries land in _error queues automatically.&lt;/p&gt;
&lt;p&gt;Health checks: expose /health and rely on container orchestration to restart unhealthy pods.&lt;/p&gt;
&lt;p&gt;Idempotency: make consumers safe to reprocess (e.g., check a processed table or use dedup keys).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;7) (Optional) Outbox &amp;amp; transactions&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you publish events within a DB transaction, consider an outbox pattern (MassTransit integrates with EFCore Outbox) to avoid dual-write issues and ensure at-least-once delivery without duplicates.&lt;/p&gt;
&lt;p&gt;8) Azure Service Bus variant (quick sketch)&lt;/p&gt;
&lt;p&gt;Swap the transport:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;builder.Services.AddMassTransit(cfg =&amp;gt;
{
    cfg.AddConsumer&amp;lt;SubmitOrderConsumer&amp;gt;();

    cfg.UsingAzureServiceBus((context, bus) =&amp;gt;
    {
        bus.Host(builder.Configuration[&quot;ASB_CONNECTION&quot;]!);

        bus.SubscriptionEndpoint&amp;lt;SubmitOrder&amp;gt;(&quot;submit-order-sub&quot;, e =&amp;gt;
        {
            e.ConfigureConsumer&amp;lt;SubmitOrderConsumer&amp;gt;(context);
            e.MaxConcurrentCalls = 8;
        });
    });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Conclusion&lt;/p&gt;
&lt;p&gt;MassTransit keeps the happy path simple while giving you the tools for serious systems: retries, sagas, scheduling, observability, and transport flexibility. Start minimal, add policies as you learn your failure modes, and keep consumers idempotent.&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/5749/masstransit-in-asp-net-core-a-practical-guide-to-event-driven-net</guid>
<pubDate>Mon, 29 Sep 2025 09:25:53 +0000</pubDate>
</item>
<item>
<title>Building a Pub/Sub System in .NET: MassTransit, Reactive Extensions, and BlockingCollection</title>
<link>https://coderlegion.com/5021/building-pub-sub-system-in-net-masstransit-reactive-extensions-and-blockingcollection</link>
<description>&lt;p&gt;Modern applications often need to broadcast events across multiple services.&lt;br&gt;
Think of an order being placed in an e-commerce system. The order service publishes an event, and multiple subscribers react independently:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;the billing service charges the customer,&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the email service sends a receipt,&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the analytics service tracks the order.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is the essence of the Publish/Subscribe (Pub/Sub) pattern.&lt;/p&gt;
&lt;p&gt;In this post we’ll explore what it is, why it matters, and three implementations in .NET:&lt;/p&gt;
&lt;p&gt;MassTransit with RabbitMQ (for distributed systems)&lt;/p&gt;
&lt;p&gt;Reactive Extensions (Rx) (for in-memory event streams)&lt;/p&gt;
&lt;p&gt;BlockingCollection (for producer/consumer pipelines)&lt;/p&gt;
&lt;p&gt; What is the Pub/Sub Pattern&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The Pub/Sub pattern decouples senders from receivers.&lt;/li&gt;
&lt;li&gt;The Publisher creates a message (e.g., OrderSubmitted) and sends it&lt;br&gt;
to a broker or channel.&lt;/li&gt;
&lt;li&gt;The Broker delivers that message to all interested Subscribers.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each Subscriber receives a copy of the message and processes it independently.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Benefits&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Decoupling — publishers do not know subscribers&lt;/li&gt;
&lt;li&gt;Scalability — multiple subscribers can process in parallel&lt;/li&gt;
&lt;li&gt;Flexibility — add or remove subscribers without changing the&lt;br&gt;
publisher&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why it matters&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This pattern has been around for decades in middleware like IBM MQ, JMS, RabbitMQ, and Kafka.&lt;br&gt;
It is fundamental in:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Event-driven microservices&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Real-time systems (chat apps, trading platforms)&lt;/li&gt;
&lt;li&gt;Notification and alerting systems&lt;/li&gt;
&lt;li&gt;Data pipelines and analytics&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;IoT device communication&lt;/p&gt;
&lt;p&gt;At its core, Pub/Sub enables loose coupling and asynchronous communication.&lt;/p&gt;
&lt;p&gt;⚙️ Hosted Services in ASP.NET Core&lt;/p&gt;
&lt;p&gt;A hosted service is a background task that runs with your app host.&lt;br&gt;
In .NET you typically inherit from BackgroundService and implement ExecuteAsync.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class SampleWorker : BackgroundService
{
    private readonly ILogger&amp;lt;SampleWorker&amp;gt; _log;
    public SampleWorker(ILogger&amp;lt;SampleWorker&amp;gt; log) =&amp;gt; _log = log;

    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        _log.LogInformation(&quot;Worker started&quot;);
        while (!ct.IsCancellationRequested)
        {
            // do work here
            await Task.Delay(TimeSpan.FromSeconds(5), ct);
        }
        _log.LogInformation(&quot;Worker stopping&quot;);
    }
}

// Register in Program.cs
builder.Services.AddHostedService&amp;lt;SampleWorker&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the &lt;a rel=&quot;nofollow&quot; href=&quot;https://masstransit.io/&quot;&gt;MassTransit&lt;/a&gt; demo below, a PublisherService is implemented as a hosted service to periodically publish messages.&lt;/p&gt;
&lt;p&gt;1️⃣ Pub/Sub with MassTransit and RabbitMQ&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;appsettings.json
{
  &quot;RabbitMq&quot;: { &quot;Host&quot;: &quot;localhost&quot;, &quot;Username&quot;: &quot;guest&quot;, &quot;Password&quot;: &quot;guest&quot; },
  &quot;PublishIntervalSeconds&quot;: 5
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OrderSubmitted.cs&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;namespace MassTransitDemo;

public record OrderSubmitted(Guid OrderId, string CustomerEmail, decimal Total);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OrderSubmittedConsumer.cs&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using MassTransit;
using Microsoft.Extensions.Logging;

namespace MassTransitDemo;

public class OrderSubmittedConsumer(ILogger&amp;lt;OrderSubmittedConsumer&amp;gt; logger) : IConsumer&amp;lt;OrderSubmitted&amp;gt;
{
    public Task Consume(ConsumeContext&amp;lt;OrderSubmitted&amp;gt; context)
    {
        var m = context.Message;
        logger.LogInformation(&quot;Consumed OrderSubmitted, OrderId {OrderId}, Email {Email}, Total {Total}&quot;,
            m.OrderId, m.CustomerEmail, m.Total);
        return Task.CompletedTask;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PublisherService.cs&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using MassTransit;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace MassTransitDemo;

public class PublisherOptions
{
    public int PublishIntervalSeconds { get; set; } = 5;
}

public class PublisherService(
    ILogger&amp;lt;PublisherService&amp;gt; logger,
    IPublishEndpoint publishEndpoint,
    IOptions&amp;lt;PublisherOptions&amp;gt; options) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var delay = TimeSpan.FromSeconds(options.Value.PublishIntervalSeconds);
        logger.LogInformation(&quot;Publisher started, interval {Seconds}s&quot;, options.Value.PublishIntervalSeconds);

        while (!stoppingToken.IsCancellationRequested)
        {
            var msg = new OrderSubmitted(Guid.NewGuid(), &quot;*Emails are not allowed*&quot;, Random.Shared.Next(50, 300));
            await publishEndpoint.Publish(msg, stoppingToken);
            logger.LogInformation(&quot;Published OrderSubmitted, OrderId {OrderId}&quot;, msg.OrderId);
            await Task.Delay(delay, stoppingToken);
        }

        logger.LogInformation(&quot;Publisher stopping&quot;);
    }
}

Program.cs

    using MassTransit;
    using MassTransitDemo;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.Hosting;
    using Microsoft.Extensions.Logging;
    
    var builder = Host.CreateApplicationBuilder(args);
    
    builder.Configuration.AddJsonFile(&quot;appsettings.json&quot;, optional: true, reloadOnChange: true);
    builder.Services.Configure&amp;lt;PublisherOptions&amp;gt;(builder.Configuration);
    
    builder.Services.AddMassTransit(mt =&amp;gt;
    {
        mt.AddConsumer&amp;lt;OrderSubmittedConsumer&amp;gt;();
    
        mt.UsingRabbitMq((ctx, cfg) =&amp;gt;
        {
            var section = builder.Configuration.GetSection(&quot;RabbitMq&quot;);
            cfg.Host(section[&quot;Host&quot;], h =&amp;gt;
            {
                h.Username(section[&quot;Username&quot;]);
                h.Password(section[&quot;Password&quot;]);
            });
    
            cfg.ReceiveEndpoint(&quot;masstransitdemo-order-submitted&quot;, e =&amp;gt;
            {
                e.ConfigureConsumer&amp;lt;OrderSubmittedConsumer&amp;gt;(ctx);
            });
    
            cfg.UseMessageRetry(r =&amp;gt; r.Interval(3, TimeSpan.FromSeconds(5)));
            cfg.UseInMemoryOutbox();
        });
    });
    
    builder.Services.AddHostedService&amp;lt;PublisherService&amp;gt;();
    
    builder.Logging.ClearProviders();
    builder.Logging.AddSimpleConsole(o =&amp;gt;
    {
        o.SingleLine = true;
        o.TimestampFormat = &quot;HH:mm:ss &quot;;
    });
    
    await builder.Build().RunAsync();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run RabbitMQ&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -it --rm -p 5672:5672 -p 15672:15672 rabbitmq:3-management
# UI at http://localhost:15672  user guest  pass guest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MassTransit is a distributed application framework for message brokers like RabbitMQ and Azure Service Bus. It takes care of serialization, consumer lifecycle, retries, and endpoint setup.&lt;/p&gt;
&lt;p&gt;2️⃣ Pub/Sub with Reactive Extensions (Rx)&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/dotnet/reactive&quot;&gt;Reactive Extensions (Rx)&lt;/a&gt;&lt;br&gt;
 is a library for composing asynchronous and event based programs with observable sequences.&lt;/p&gt;
&lt;p&gt;Uses IObservable (event source) and IObserver (subscriber)&lt;/p&gt;
&lt;p&gt;Provides LINQ-style operators (Where, Select, Buffer) for event streams&lt;/p&gt;
&lt;p&gt;Perfect for in-memory event pipelines and UI scenarios&lt;/p&gt;
&lt;p&gt;⚠️ Limitation: Rx is in-memory only. Events vanish when the app stops.&lt;/p&gt;
&lt;p&gt;Program.cs&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using System;
using System.Reactive.Linq;
using System.Reactive.Subjects;

public record OrderSubmitted(Guid OrderId, string CustomerEmail, decimal Total);

class Program
{
    static void Main()
    {
        var bus = new Subject&amp;lt;OrderSubmitted&amp;gt;();

        // Subscriber A
        var subA = bus.Subscribe(order =&amp;gt;
            Console.WriteLine($&quot;[Email] Send receipt to {order.CustomerEmail}&quot;));

        // Subscriber B with filter and transform
        var subB = bus
            .Where(o =&amp;gt; o.Total &amp;gt;= 100)
            .Select(o =&amp;gt; new { o.OrderId, Vat = o.Total * 0.24m })
            .Subscribe(x =&amp;gt;
                Console.WriteLine($&quot;[Analytics] Order {x.OrderId}, VAT {x.Vat:F2}&quot;));

        // Publish a few messages
        bus.OnNext(new OrderSubmitted(Guid.NewGuid(), &quot;*Emails are not allowed*&quot;, 79.90m));
        bus.OnNext(new OrderSubmitted(Guid.NewGuid(), &quot;*Emails are not allowed*&quot;, 149.00m));
        bus.OnNext(new OrderSubmitted(Guid.NewGuid(), &quot;*Emails are not allowed*&quot;, 220.00m));

        bus.OnCompleted();
        subA.Dispose();
        subB.Dispose();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3️⃣ Pub/Sub with BlockingCollection&lt;/p&gt;
&lt;p&gt;BlockingCollection is a thread safe producer and consumer collection in .NET.&lt;/p&gt;
&lt;p&gt;Supports blocking (consumers wait until data is available)&lt;/p&gt;
&lt;p&gt;Supports bounding (limit capacity to avoid flooding)&lt;/p&gt;
&lt;p&gt;Useful for background workers, pipelines, or batch processors&lt;/p&gt;
&lt;p&gt;Program.cs&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using System.Collections.Concurrent;

public record OrderSubmitted(Guid OrderId, string CustomerEmail, decimal Total);

class Program
{
    static async Task Main()
    {
        using var queue = new BlockingCollection&amp;lt;OrderSubmitted&amp;gt;(boundedCapacity: 100);

        // Subscriber A
        var emailTask = Task.Run(() =&amp;gt;
        {
            foreach (var order in queue.GetConsumingEnumerable())
                Console.WriteLine($&quot;[Email] Send receipt to {order.CustomerEmail}&quot;);
        });

        // Subscriber B
        var analyticsTask = Task.Run(() =&amp;gt;
        {
            foreach (var order in queue.GetConsumingEnumerable())
                Console.WriteLine($&quot;[Analytics] Track order {order.OrderId}, total {order.Total}&quot;);
        });

        // Publisher
        foreach (var price in new[] { 49.50m, 129.00m, 250.00m, 15.00m })
        {
            queue.Add(new OrderSubmitted(Guid.NewGuid(), $&quot;user{price}@example.com&quot;, price));
        }

        queue.CompleteAdding();
        await Task.WhenAll(emailTask, analyticsTask);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⚠️ Limitation: Only works inside a single process. No distribution.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=250057942209870061&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt; References&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://masstransit.io/&quot;&gt;MassTransit Documentation&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MassTransit/MassTransit&quot;&gt;MassTransit GitHub&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rabbitmq.com/tutorials/tutorial-one-dotnet.html&quot;&gt;RabbitMQ Tutorials&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/dotnet/core/extensions/workers&quot;&gt;Microsoft Docs: Worker Services&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/azure/architecture/patterns/publisher-subscriber&quot;&gt;Microsoft Docs: Publisher-Subscriber Pattern&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/dotnet/reactive&quot;&gt;System.Reactive GitHub&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/dotnet/standard/collections/thread-safe/blockingcollection-overview&quot;&gt;BlockingCollection Overview&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;✅ With these three approaches, you now have the full spectrum of Pub/Sub in .NET:&lt;/p&gt;
&lt;p&gt;Lightweight in-memory streams with Rx and BlockingCollection&lt;/p&gt;
&lt;p&gt;Robust distributed messaging with MassTransit and RabbitMQ&lt;/p&gt;
&lt;p&gt;The Pub/Sub pattern has lasted decades because it makes systems more modular, more scalable, and more resilient to change.&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/5021/building-pub-sub-system-in-net-masstransit-reactive-extensions-and-blockingcollection</guid>
<pubDate>Tue, 26 Aug 2025 13:57:29 +0000</pubDate>
</item>
<item>
<title>Building an Order Processing Saga with MassTransit</title>
<link>https://coderlegion.com/4621/building-an-order-processing-saga-with-masstransit</link>
<description>&lt;p&gt;MassTransit has been around since 2007 and has matured into one of the most stable and feature-rich open-source .NET messaging frameworks.&lt;/p&gt;
&lt;p&gt;It acts like a factory for message brokers, meaning you can switch between providers such as RabbitMQ, Azure Service Bus, ActiveMQ, Amazon SQS, and others with minimal configuration changes.&lt;/p&gt;
&lt;p&gt;This makes it ideal for enterprise-grade distributed systems where you want flexibility without vendor lock-in.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why MassTransit?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Stability: Over a decade of continuous development and production use.&lt;/p&gt;
&lt;p&gt;Multi-Broker Support: Easily swap between Azure Service Bus, RabbitMQ, Amazon SQS, etc., without rewriting business logic.&lt;/p&gt;
&lt;p&gt;Developer Friendly: Built for .NET developers with excellent documentation and integration with DI, EF Core, and ASP.NET Core.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Advanced Features:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Outbox pattern&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Request/Response messaging&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Retry and circuit-breaker policies&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sagas and State Machines&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Message scheduling&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;One of the Coolest Features: The State Machine&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A State Machine allows you to model complex workflows where messages arrive in any order, but your application processes them in a controlled, state-aware sequence.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=8178334018060771430&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;For example, in this Order Processing Saga:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;An order is submitted.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The saga waits for stock confirmation.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If stock is reserved, the saga waits for payment authorization.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Once payment is authorized, the order is marked as completed.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If any step fails, the saga can transition to a failed state and trigger compensating actions.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This approach eliminates spaghetti code for multi-step processes and makes workflows easy to visualize and maintain.&lt;/p&gt;
&lt;p&gt;Technology Stack Used in This Project&lt;br&gt;
.NET 9&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;MassTransit (with Saga State Machine)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;RabbitMQ (message broker)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;PostgreSQL (saga persistence)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Entity Framework Core (database access)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Docker (local RabbitMQ/PostgreSQL setup)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Source Code&lt;br&gt;
Full working implementation available here:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/OutboxSagaDemo&quot;&gt; GitHub – OutboxSagaDemo&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;MassTransit Documentation – &lt;a rel=&quot;nofollow&quot; href=&quot;https://masstransit.io/&quot;&gt;https://masstransit.io/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;MassTransit GitHub Repository – &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MassTransit/MassTransit&quot;&gt;https://github.com/MassTransit/MassTransit&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;MassTransit Saga State Machine Guide – &lt;a rel=&quot;nofollow&quot; href=&quot;https://masstransit.io/documentation/patterns/saga&quot;&gt;https://masstransit.io/documentation/patterns/saga&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;RabbitMQ Documentation – &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rabbitmq.com/documentation.html&quot;&gt;https://www.rabbitmq.com/documentation.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;PostgreSQL Documentation – &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.postgresql.org/docs/&quot;&gt;https://www.postgresql.org/docs/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Entity Framework Core Docs – &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/&quot;&gt;https://learn.microsoft.com/en-us/ef/core/&lt;/a&gt;&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/4621/building-an-order-processing-saga-with-masstransit</guid>
<pubDate>Tue, 12 Aug 2025 09:22:53 +0000</pubDate>
</item>
</channel>
</rss>