Stop Gluing APIs Together - Build Real Systems with Event-Driven Architecture (.NET Example)
Most microservice systems end up as tangled messes of REST calls and dependency nightmares. In this article, we’ll build something better - an event-driven .NET 8 system using RabbitMQ and MassTransit that keeps services loosely coupled, fault-tolerant, and actually enjoyable to maintain.
Using RabbitMQ, MassTransit, and .NET 8 to Create Loosely Coupled Services That Actually Scale
“Just have Service A call Service B’s endpoint directly.”
– Every junior dev, five minutes before discovering circular dependencies, deadlocks, and regret.
We’ve all done it: you build a small API that grows, then another service that needs to talk to it, and before long your architecture looks like a tangled Christmas tree plugged into a single overloaded socket. Everything depends on everything else, deploys take hours, and one timeout in the chain drags the whole circus down.
This is where event-driven architecture (EDA) quietly walks in, slaps you on the back, and says:
“Stop shouting HTTP requests at each other. Just send a message.”
Let’s explore how this looks in .NET 8 using a working example — my OrderProcessingSystem repo. It’s a lightweight but fully functional distributed app using RabbitMQ, MassTransit, and Entity Framework Core, built to show how real systems can stay simple, resilient, and fun to build.
🧩 The Problem: API Chains from Hell
The traditional microservice mindset often starts innocent:
- “User places an order.”
- “Order Service calls Payment Service.”
- “Payment Service calls Inventory Service.”
- “Inventory Service calls Notification Service.”
Congratulations — you’ve built a distributed monolith. Everything depends on everything, and when Payment goes down, your entire system becomes a glorified paperweight.
This is where message-based communication shines. Instead of tightly coupled API calls, services communicate by publishing and subscribing to events. They don’t care who listens — they just announce what happened.
⚙️ The Fix: Events, Not Endpoints
In the OrderProcessingSystem, each service does one thing well:
| Service | Responsibility | Listens For | Publishes |
|---|---|---|---|
| OrderService | Accepts orders, persists them | – | OrderPlacedEvent |
| PaymentService | Simulates charging the customer | OrderPlacedEvent | PaymentSucceededEvent, PaymentFailedEvent |
| InventoryService | Updates stock levels | OrderPlacedEvent | InventoryAdjustedEvent |
| NotificationService | Sends user notifications | PaymentSucceededEvent, PaymentFailedEvent | – |
Every service owns its data and logic. They don’t call each other. They just emit facts — and the rest of the system reacts.
🐇 RabbitMQ + MassTransit = Peace of Mind
For the uninitiated:
- RabbitMQ is the message broker — the reliable post office delivering your events.
- MassTransit is the friendly library that makes it painless to integrate RabbitMQ into your .NET apps.
Instead of hand-rolling queue consumers, acknowledgments, and retry logic, MassTransit gives you a nice API like:
public class OrderPlacedConsumer : IConsumer<OrderPlacedEvent>
{
public async Task Consume(ConsumeContext<OrderPlacedEvent> context)
{
Console.WriteLine($"Order received: {context.Message.OrderId}");
// Do your business logic here
}
}
Boom — you’re now consuming events from RabbitMQ without touching a line of AMQP boilerplate.
🧠 Inside the Flow
Here’s how the system behaves from start to finish:
- OrderService receives a new order via HTTP:
POST /api/orders
{
"productId": "123",
"quantity": 2,
"userId": "42"
}
It saves the record to PostgreSQL (using EF Core), and then publishes an OrderPlacedEvent.
2. PaymentService and InventoryService both subscribe to that event:
- PaymentService simulates a transaction.
- InventoryService deducts stock.
3. PaymentService emits either a success or failure event.
4. NotificationService listens for those results and “notifies” the user (logs, emails, smoke signals, whatever).
Notice the beauty: none of these services know about each other.
If you add a LoyaltyService tomorrow that gives users points for purchases, just subscribe to PaymentSucceededEvent. No other service changes.
🧾 The Code Structure
The repo looks like this:
OrderProcessingSystem/
│
├── Shared.Contracts/
│ └── Events/
│ ├── OrderPlacedEvent.cs
│ ├── PaymentSucceededEvent.cs
│ └── PaymentFailedEvent.cs
│
├── OrderService/
├── PaymentService/
├── InventoryService/
└── NotificationService/
Each project is a standalone .NET 8 service.
They all share one Contracts library that defines event schemas — the “language” everyone agrees on.
You spin up the infra (RabbitMQ + Postgres) using Docker, then run each service with dotnet run. Swagger docs are included for testing API endpoints.
🧩 Why It Matters
This repo isn’t just an academic toy. It’s a slice of real-world architecture boiled down to something you can actually reason about.
Here’s what you learn by building or running it:
✅ Loose coupling done right
Each service can live, die, or redeploy without breaking others.
Event-driven systems degrade gracefully instead of catastrophically.
✅ Scalability baked in
Want to handle 10x the order volume? Spin up more consumers — RabbitMQ will distribute messages automatically.
✅ Error isolation
If PaymentService goes rogue, the other services keep humming. When it recovers, RabbitMQ delivers pending events automatically.
✅ Future-proofing
Adding features means adding listeners, not rewiring your entire codebase.
🧪 A Practical Lesson in CQRS
Inside each service, I use MediatR to handle commands and queries separately.
It’s a CQRS-lite setup that keeps controller actions clean:
[HttpPost("orders")]
public async Task<IActionResult> PlaceOrder(CreateOrderCommand command)
{
var orderId = await _mediator.Send(command);
return Ok(new { orderId });
}
This pattern forces you to stop writing God-Controllers that do everything from DB writes to business logic to notifications.
🔍 Where to Go From Here
The repo is a living thing — here’s what’s on the roadmap:
- 🪶 OpenTelemetry tracing across all services
- 🧰 Docker Compose orchestration for one-command startup
- 🧾 Centralized logging (Serilog + Seq)
- 🔐 JWT authentication between services
- 🧪 Integration testing for end-to-end validation
Each of these adds a new layer of realism — making this repo not just “cute demo code,” but a genuine architectural blueprint.
💡 Why You Should Care (Even If You’re Not Building Microservices)
You might think:
“I’m not Netflix. Why would I need RabbitMQ?”
Because even small systems benefit from decoupled thinking.
You don’t need five services — you can apply the same event-driven logic inside one application using in-memory buses or message brokers.
If you’ve ever wanted:
- Better fault tolerance
- Less code spaghetti
- Async background workflows that don’t block APIs
...then you’re already halfway to event-driven design.
⚙️ Run It Yourself
Clone the repo:
git clone https://github.com/Ciaran-Codes/OrderProcessingSystem.git
cd OrderProcessingSystem
Start RabbitMQ and Postgres:
docker-compose up -d
Run each service:
dotnet run --project OrderService
dotnet run --project PaymentService
dotnet run --project InventoryService
dotnet run --project NotificationService
Then hit:
http://localhost:5000/swagger
Place an order, and watch the chain reaction unfold in your console logs.
🚀 Final Thoughts
Event-driven architecture isn’t magic. It’s just good decoupling.
It’s also one of those concepts that separates developers who build features from engineers who build systems.
By shifting from “call this API directly” to “publish an event and let others care,” you unlock flexibility, resilience, and sanity — three things enterprise systems are chronically short on.
If you’ve ever thought,
“There has to be a better way than chaining 12 REST calls together and hoping for the best,”
there is. And it looks a lot like this repo