Most teams reach for event-driven architecture (EDA) because they heard it "scales." Then they discover the hard part: events arrive out of order, get delivered twice, and a bug three services away surfaces as a customer complaint with no stack trace. EDA is powerful — and it trades one set of problems for another.
This is the honest, production version for .NET. This is the condensed take; the full guide (complete architecture diagram, real MassTransit code, the React SignalR live tracker, the tuning playbook, and the when-not-to section) is on my site 👇
Full guide: https://prepstack.co.in/blog/event-driven-architecture-aspnet-core-react-production-guide
What EDA actually is
In request/response, OrderService → (blocks) → PaymentService → (blocks) → InventoryService. If any link is slow or down, the whole chain stalls. In EDA, OrderService emits a fact — OrderPlaced — and moves on. Payment, inventory, email, and analytics all react independently through a broker. The producer doesn't know who's listening, doesn't wait, and doesn't break if a consumer is down. That decoupling is the whole point — and everything good and hard about EDA flows from it.
What it buys you
-
Kills tight coupling. Want a Slack alert on new orders?
SlackServicesubscribes toOrderPlaced.OrderServicenever changes. New consumers are added, not integrated — open/closed at the architecture level. -
Kills blocking latency. Checkout emits
OrderPlacedand returns a202in tens of ms; the downstream work runs in parallel, async. - Handles spiky load. A flash sale queues in the broker (load leveling); consumers process at their own pace and you scale the bottleneck one independently.
- Independent deployment + a free audit log (the event stream is the history).
What it costs (be honest)
- Eventual consistency — the order isn't confirmed yet. If your domain needs strong consistency, EDA fights you.
- Duplicate delivery — brokers guarantee at-least-once, not exactly-once. The same event will arrive twice. Every consumer with side effects must be idempotent.
- Out-of-order delivery — don't assume order; partition by entity key when ordering matters.
- Debugging across the boundary — the failure is "the order never confirmed," cause three hops away. Distributed tracing is mandatory.
- Operational complexity — a broker to run, queues to monitor, DLQs to drain.
The two non-negotiables
1. The Outbox. The classic bug: save the order, then publish the event — but the process crashes between them. Now you have an order with no event. Fix: write the event into the same DB transaction as the business data, then a dispatcher publishes it after commit.
db.Orders.Add(order);
await publish.Publish(new OrderPlaced(order.Id, /* ... */)); // through the Outbox
await db.SaveChangesAsync(); // ONE commit: order + outbox event
return Results.Accepted($"/orders/{order.Id}", new { orderId = order.Id, status = "Pending" });
2. Idempotent consumers. At-least-once + retries guarantee duplicates. Charging twice is unacceptable — dedupe on message ID:
if (await _seen.AlreadyProcessed(ctx.MessageId!.Value)) return; // skip duplicate
var result = await _gateway.ChargeAsync(msg.Total, msg.CustomerId);
await _seen.MarkProcessed(ctx.MessageId.Value);
The React payoff (SignalR, no polling)
NotifyService consumes OrderConfirmed and pushes it to the browser over SignalR; a useOrderStatus hook flips the UI from "Pending" to "Confirmed" in real time as the async events flow through — no polling. The user saw a fast 202, then watches the order tick to Confirmed live.
The production levers (each with a reason)
| Lever | Buys you | Trade-off |
|---|---|---|
| Outbox | No lost events | One extra table + dispatcher |
| Idempotency | Safe aggressive retries | A dedupe store (Redis) |
| Prefetch + concurrency | Per-instance throughput | Tune carefully or OOM |
| Partitioning (by OrderId) | Ordering + parallelism | Key design; can't reorder later |
| KEDA autoscaling | Elastic capacity | K8s + autoscaler setup |
| DLQ + alerts | Poison isolation + visibility | Ops process to drain DLQ |
| Backpressure | Stability under spike | Lower peak throughput |
When NOT to use it
Simple CRUD (a synchronous monolith is faster to build and easier to debug). Strong-consistency transactions (a bank balance correct the instant the call returns). Small teams without ops maturity. Low traffic with no scale problem. In all of these, EDA is complexity with no payoff.
The decision rule: adopt EDA when the cost of coupling and blocking exceeds the cost of eventual consistency and operational complexity. If you can't clearly say which side is heavier, you probably don't need it yet.
The full guide has the complete architecture diagram, the full MassTransit + Azure Service Bus code (Outbox, idempotent consumer, saga completion), the React SignalR tracker end-to-end, all 8 tuning levers with reasoning, and the 10-step incremental adoption playbook:
https://prepstack.co.in/blog/event-driven-architecture-aspnet-core-react-production-guide
Originally published on PrepStack.
Top comments (0)