Search for a command to run...

Somewhere along the way, CQRS with MediatR stopped being a design decision and became the default scaffolding for .NET backends. New project, Clean Architecture template, MediatR installed before the first entity exists. I've used this stack in two real systems — my blog platform, which runs in production, and my graduation project — and I deliberately left it out of a third. The difference between those choices is the part the tutorials skip.
Strip away the pattern diagrams and MediatR is an in-process mediator: you send a request object, exactly one handler answers it, and neither side references the other directly. That decoupling is nice, but it isn't the real win. The real win is the pipeline. Cross-cutting concerns — validation, logging, transaction boundaries — wrap every request in behaviors, written once, applied everywhere.
In my blog's backend, every command and query passes through a validation behavior before it touches a handler:
FluentValidation validators are registered per request type, the behavior runs them all, and an invalid request never reaches business logic. My handlers assume valid input — not because I trust callers, but because the pipeline guarantees it. That's the trade MediatR offers at its best: a small amount of indirection in exchange for cross-cutting concerns living in exactly one place.
The second win is shape. One handler per use case makes the application layer read like a table of contents:
No god-service with forty methods. Each use case is a small, testable unit, and adding a feature means adding files, not editing a shared service until it resists change.
Validation is the behavior everyone demos — and in El-Hakim's codebase today, it is the only one registered. The transaction behavior is the other half of this design, the one I reach for when a command has to succeed or fail as a single unit: every such command runs inside a database transaction — commit if the handler completes, roll everything back if it throws.
Why does this live in the pipeline instead of inside each handler? Because a transaction boundary is a property of the use case, not of any single handler. If every handler opened its own transaction, some command would eventually execute half inside one and half outside, because someone forgot. The pipeline makes the rule un-skippable: every command, always, and no handler author has to remember.
El-Hakim is my personal platform — blog, portfolio, job applications, comments, media, likes. It runs Clean Architecture on .NET 10, and the Application layer is organized as commands and queries through MediatR. The feature count is exactly the situation the pattern suits: many use cases, shared validation rules, and a codebase I maintain alone, where consistency matters more than cleverness. When every feature enters through the same pipeline, I never have to remember which controller validates and which one forgot.
Most CQRS writing is about writes. Half my traffic is reads, and the query side is where the separation earns its keep. This is the query behind the blog's post list:
Three things matter here. AsNoTracking, because a read that will never be written back shouldn't pay for change tracking. The projection happens inside the query, so the database returns exactly the DTO the page needs — no entity materialized just to be mapped and thrown away. And pagination happens at the source, before a single row crosses the wire. Queries don't need behaviors stacked on them, and they don't need to travel the same road commands do. That asymmetry is the actual point of splitting reads from writes.
AIO Academic Hub, my graduation project, is where I learned the limits. Under simultaneous exam submissions, the system hit database lock contention. We applied CQRS with MediatR — but I want to be precise about what fixed what. Separating the write path made the submission flow explicit, and that explicitness is what let us attach the things that actually relieved the contention: Redis session caching and persistent background queues for the heavy processing. CQRS didn't remove a single lock. It gave us a seam where the fix could go. That's a more modest claim than most CQRS write-ups make, and it's the honest one.
GeoGuard is a .NET API I built as a technical assessment: country-based IP blocking with a hard constraint — no database. All state lives in thread-safe in-memory collections, a ConcurrentDictionary for blocked countries and a ConcurrentQueue for the audit log of access attempts. The use-case surface is small: block a country, unblock it, check an IP, read the attempt log.
For that surface, MediatR would have been cargo cult. I wrote in the project's README that a dedicated Application layer was deliberately omitted — the use cases are straightforward enough that splitting them out would create unnecessary abstraction and plumbing, so the domain encompasses the orchestration. A plain service does the work:
And this is the store underneath it — the entire persistence layer for country blocking:
That's it. No DbContext, no migrations, no repository-over-EF ceremony — a thread-safe dictionary behind an interface, because the assessment's no-database constraint made in-memory the honest design. If that constraint ever changes, the interface is the seam; the service above it doesn't move.
No mediator, no handlers, no validators — and also no AutoMapper or FluentValidation, for the same reason. The code is shorter, the call stack is honest, and a reviewer can read the whole flow in one sitting. Leaving a pattern out was a design decision, documented like one.
MediatR's costs are real, they're just quiet. Debugging means stepping through the mediator instead of jumping straight to the implementation; "go to definition" lands on an interface, not the handler. Every use case costs you several files — request, handler, validator — which is a fair price at fifty use cases and pure ceremony at five. And the pattern name itself causes confusion: CQRS is an architectural idea about separating reads from writes; MediatR is a library. You can split your read and write models without a mediator, and you can use a mediator with a single shared model — most "CQRS" codebases, mine included, do the latter and should say so.
The failure mode I see most is the CRUD app wearing enterprise clothing: four entities, basic create-read-update-delete, and a full CQRS pipeline adding latency to development, not removing complexity from the system. The pattern was invented for systems where the write side and the read side genuinely pull in different directions. If yours don't, the pipeline is a toll booth on a road with no traffic.
Reach for MediatR when cross-cutting behaviors — validation, auditing, transactions — need one home across many use cases.
Reach for it when the feature count keeps growing and a shared service class is turning into a junk drawer.
Skip it when the use cases are few, straightforward, and unlikely to multiply — a plain service is easier to read, debug, and delete.
Skip it when the constraint is smallness itself: assessments, prototypes, single-purpose services.
Laid side by side, the three systems make the argument better than any diagram:
El-Hakim → MediatR pipeline. Many features, shared validation rules, one maintainer. Consistency wins, so the pipeline pays for itself.
AIO Academic Hub → CQRS as the seam. It fixed no locks by itself; it made Redis caching and background queues attachable without restructuring the system.
GeoGuard → plain services, no mediator. Four use cases and a hard no-database constraint. A pipeline there would have been weight without payoff.
Patterns are tools with price tags. CQRS with MediatR has paid for itself in two of my systems and would have taxed the third for nothing. The skill isn't knowing the pattern — it's knowing which of your systems is which.
I've shipped CQRS with MediatR in two real systems — and deliberately left it out of a third. The pattern isn't the point; knowing where its boundary sits is.
In Part 1, we traced the journey of a request from browser to server. Now, we open the engine. This deep dive explores how the Hakim backend is structured using Clean Architecture and CQRS via MediatR — from compiled EF Core queries and Redis caching to validation pipelines and domain events. A look at why enterprise-grade patterns matter, even for a personal blog.
Explore the low-level execution pipeline of .NET: from Roslyn CIL compilation down to native CPU assembly. Learn how Method Table Trampolines, Tiered Compilation, Dynamic PGO, On-Stack Replacement (OSR), and Native AOT optimize modern high-performance C# applications.